diff --git a/apps/desktop/layer/renderer/src/modules/settings/tabs/ai.tsx b/apps/desktop/layer/renderer/src/modules/settings/tabs/ai.tsx
index 7e1bae11e..16fe3e757 100644
--- a/apps/desktop/layer/renderer/src/modules/settings/tabs/ai.tsx
+++ b/apps/desktop/layer/renderer/src/modules/settings/tabs/ai.tsx
@@ -1,51 +1,14 @@
-import { Spring } from "@follow/components/constants/spring.js"
-import { Button } from "@follow/components/ui/button/index.js"
-import { Input, TextArea } from "@follow/components/ui/input/index.js"
-import { KbdCombined } from "@follow/components/ui/kbd/Kbd.js"
-import { KeyValueEditor } from "@follow/components/ui/key-value-editor/index.js"
-import { Label } from "@follow/components/ui/label/index.jsx"
-import { Progress } from "@follow/components/ui/progress/index.jsx"
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from "@follow/components/ui/select/index.js"
-import { Switch } from "@follow/components/ui/switch/index.jsx"
-import type { AIShortcut, MCPService } from "@follow/shared/settings/interface"
-import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
-import { AnimatePresence } from "motion/react"
-import * as React from "react"
-import { useState } from "react"
import { useTranslation } from "react-i18next"
-import { toast } from "sonner"
-import {
- AIChatPanelStyle,
- setAIChatPanelStyle,
- setAISetting,
- setMCPEnabled,
- useAIChatPanelStyle,
- useAISettingValue,
- useMCPEnabled,
-} from "~/atoms/settings/ai"
-import { m } from "~/components/common/Motion"
-import { useDialog, useModalStack } from "~/components/ui/modal/stacked/hooks"
-import { apiFetch } from "~/lib/api-fetch"
-import {
- createMCPConnection,
- deleteMCPConnection,
- fetchMCPConnections,
- mcpQueryKeys,
- refreshMCPTools,
- updateMCPConnection,
-} from "~/queries/mcp"
+import { setAISetting, useAISettingValue } from "~/atoms/settings/ai"
-import { SettingActionItem, SettingDescription, SettingTabbedSegment } from "../control"
import { createDefineSettingItem } from "../helper/builder"
import { createSettingBuilder } from "../helper/setting-builder"
-import { SettingModalContentPortal } from "../modal/layout"
+import { MCPServicesSection } from "./ai/mcp/MCPServicesSection"
+import { PanelStyleSection } from "./ai/PanelStyleSection"
+import { PersonalizePromptSection } from "./ai/PersonalizePromptSection"
+import { AIShortcutsSection } from "./ai/shortcuts/AIShortcutsSection"
+import { TokenUsageSection } from "./ai/TokenUsageSection"
const SettingBuilder = createSettingBuilder(useAISettingValue)
const defineSettingItem = createDefineSettingItem(useAISettingValue, setAISetting)
@@ -67,7 +30,7 @@ export const SettingAI = () => {
value: t("features.title"),
},
- PanelStyleSegment,
+ PanelStyleSection,
defineSettingItem("autoScrollWhenStreaming", {
label: t("settings.autoScrollWhenStreaming.label"),
description: t("settings.autoScrollWhenStreaming.description"),
@@ -78,7 +41,7 @@ export const SettingAI = () => {
value: t("personalize.title"),
},
- PersonalizePromptSetting,
+ PersonalizePromptSection,
{
type: "title",
@@ -96,932 +59,3 @@ export const SettingAI = () => {
)
}
-
-const useTokenUsage = () => {
- return useQuery({
- queryKey: ["aiTokenUsage"],
- queryFn: async () => {
- // TODO: replace with api client call
- return (
- (await apiFetch("/ai/usage")) as {
- code: 0
- data: {
- total: number
- used: number
- remaining: number
- resetAt: string
- }
- }
- ).data
- },
- })
-}
-
-const TokenUsageSection = () => {
- const { t } = useTranslation("ai")
-
- const tokenUsage = useTokenUsage().data || {
- total: 0,
- used: 0,
- remaining: 0,
- resetAt: new Date(),
- }
-
- const usagePercentage = tokenUsage.total === 0 ? 0 : (tokenUsage.used / tokenUsage.total) * 100
- const resetDate = new Date(tokenUsage.resetAt)
-
- return (
-
-
{t("token_usage.description")}
-
-
-
-
-
- {t("token_usage.tokens_used", {
- used: tokenUsage.used.toLocaleString(),
- total: tokenUsage.total.toLocaleString(),
- })}
-
-
- {tokenUsage.remaining.toLocaleString()} {t("token_usage.tokens_remaining")}
-
-
-
-
-
{Math.round(usagePercentage)}%
-
-
-
-
-
-
-
- 0
-
- {t("token_usage.resets_at")}: {resetDate.toLocaleDateString()}
-
- {tokenUsage.total.toLocaleString()}
-
-
-
-
-
-
- )
-}
-
-const PersonalizePromptSetting = () => {
- const { t } = useTranslation("ai")
- const aiSettings = useAISettingValue()
- const [prompt, setPrompt] = useState(aiSettings.personalizePrompt)
- const [isSaving, setIsSaving] = useState(false)
-
- const MAX_CHARACTERS = 500
- const currentLength = prompt.length
- const isOverLimit = currentLength > MAX_CHARACTERS
- const hasChanges = prompt !== aiSettings.personalizePrompt
-
- const handlePromptChange = (e: React.ChangeEvent) => {
- const { value } = e.target
- // Allow typing but show validation error if over limit
- setPrompt(value)
- }
-
- const handleSave = async () => {
- if (isOverLimit) {
- toast.error(`Prompt must be ${MAX_CHARACTERS} characters or less`)
- return
- }
-
- setIsSaving(true)
- try {
- setAISetting("personalizePrompt", prompt)
- toast.success(t("personalize.saved"))
- } finally {
- setIsSaving(false)
- }
- }
-
- return (
-
-
-
-
-
-
MAX_CHARACTERS * 0.8
- ? "text-yellow"
- : "text-text-tertiary"
- }`}
- >
- {currentLength}/{MAX_CHARACTERS}
-
-
-
- {t("personalize.prompt.help")}
- {isOverLimit && (
-
- Prompt exceeds {MAX_CHARACTERS} character limit
-
- )}
-
-
-
-
- {hasChanges && (
-
-
-
- Unsaved changes
-
-
-
-
- )}
-
-
- )
-}
-
-const AIShortcutsSection = () => {
- const { t } = useTranslation("ai")
- const { shortcuts } = useAISettingValue()
- const { present } = useModalStack()
-
- const handleAddShortcut = () => {
- present({
- title: "Add AI Shortcut",
- content: ({ dismiss }: { dismiss: () => void }) => (
- {
- const newShortcut: AIShortcut = {
- ...shortcut,
- id: Date.now().toString(),
- }
- setAISetting("shortcuts", [...shortcuts, newShortcut])
- toast.success(t("shortcuts.added"))
- dismiss()
- }}
- onCancel={dismiss}
- />
- ),
- })
- }
-
- const handleEditShortcut = (shortcut: AIShortcut) => {
- present({
- title: "Edit AI Shortcut",
- content: ({ dismiss }: { dismiss: () => void }) => (
- {
- setAISetting(
- "shortcuts",
- shortcuts.map((s) =>
- s.id === shortcut.id ? { ...updatedShortcut, id: shortcut.id } : s,
- ),
- )
- toast.success(t("shortcuts.updated"))
- dismiss()
- }}
- onCancel={dismiss}
- />
- ),
- })
- }
-
- const handleDeleteShortcut = (id: string) => {
- setAISetting(
- "shortcuts",
- shortcuts.filter((s) => s.id !== id),
- )
- toast.success(t("shortcuts.deleted"))
- }
-
- const handleToggleShortcut = (id: string, enabled: boolean) => {
- setAISetting(
- "shortcuts",
- shortcuts.map((s) => (s.id === id ? { ...s, enabled } : s)),
- )
- }
-
- return (
-
-
-
- {shortcuts.length === 0 && (
-
-
-
-
-
{t("shortcuts.empty.title")}
-
{t("shortcuts.empty.description")}
-
- )}
-
- {shortcuts.map((shortcut) => (
-
- ))}
-
- )
-}
-
-interface ShortcutItemProps {
- shortcut: AIShortcut
- onDelete: (id: string) => void
- onToggle: (id: string, enabled: boolean) => void
- onEdit: (shortcut: AIShortcut) => void
-}
-
-const ShortcutItem = ({ shortcut, onDelete, onToggle, onEdit }: ShortcutItemProps) => {
- return (
-
-
-
-
-
{shortcut.name}
- {shortcut.hotkey && (
-
- {shortcut.hotkey}
-
- )}
-
-
- {shortcut.prompt}
-
-
-
-
-
-
-
-
-
-
-
- {shortcut.enabled ? "ON" : "OFF"}
-
- onToggle(shortcut.id, enabled)}
- />
-
-
-
-
- )
-}
-
-interface ShortcutModalContentProps {
- shortcut?: AIShortcut | null
- onSave: (shortcut: Omit) => void
- onCancel: () => void
-}
-
-const ShortcutModalContent = ({ shortcut, onSave, onCancel }: ShortcutModalContentProps) => {
- const { t } = useTranslation("ai")
- const [name, setName] = useState(shortcut?.name || "")
- const [prompt, setPrompt] = useState(shortcut?.prompt || "")
-
- const [enabled, setEnabled] = useState(shortcut?.enabled ?? true)
-
- const handleSave = () => {
- if (!name.trim() || !prompt.trim()) {
- toast.error(t("shortcuts.validation.required"))
- return
- }
-
- onSave({
- name: name.trim(),
- prompt: prompt.trim(),
- enabled,
- })
- }
-
- return (
-
-
-
-
- setName(e.target.value)}
- placeholder={t("shortcuts.name_placeholder")}
- />
-
- {/*
-
-
-
*/}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- )
-}
-
-const MCPServicesSection = () => {
- const { t } = useTranslation("ai")
- const mcpEnabled = useMCPEnabled()
- const queryClient = useQueryClient()
- const dialog = useDialog()
-
- // Reusable OAuth authorization handler using dialog
- const handleOAuthAuthorization = async (authorizationUrl: string) => {
- const confirmed = await dialog.ask({
- title: t("integration.mcp.service.auth_required"),
- message: t("integration.mcp.service.auth_message"),
- confirmText: t("integration.mcp.service.open_auth"),
- cancelText: t("words.cancel", { ns: "common" }),
- variant: "ask",
- })
-
- if (confirmed) {
- const popup = window.open(
- authorizationUrl,
- "_blank",
- "width=600,height=700,scrollbars=yes,resizable=yes",
- )
- if (!popup) {
- toast.error(t("integration.mcp.service.popup_blocked"))
- } else {
- toast.success(t("integration.mcp.service.auth_window_opened"))
- }
- }
- }
-
- // Query for MCP connections
- const {
- data: mcpServices = [],
- isLoading,
- error,
- refetch,
- } = useQuery({
- queryKey: mcpQueryKeys.connections(),
- queryFn: fetchMCPConnections,
- enabled: mcpEnabled,
- refetchInterval: 30_000,
- refetchOnWindowFocus: true,
- retry: 2,
- })
-
- // Mutation for creating MCP connection
- const createConnectionMutation = useMutation({
- mutationFn: createMCPConnection,
- onSuccess: async (result) => {
- queryClient.invalidateQueries({ queryKey: mcpQueryKeys.connections() })
-
- // Handle OAuth authorization if needed
- if (result.authorizationUrl) {
- await handleOAuthAuthorization(result.authorizationUrl)
- } else {
- toast.success(t("integration.mcp.service.added"))
- }
- },
- onError: (error) => {
- toast.error(t("integration.mcp.service.discovery_failed"))
- console.error("Failed to create MCP connection:", error)
- },
- })
-
- // Mutation for updating MCP connection
- const updateConnectionMutation = useMutation({
- mutationFn: ({
- connectionId,
- updateData,
- }: {
- connectionId: string
- updateData: Parameters[1]
- }) => updateMCPConnection(connectionId, updateData),
- onSuccess: async (result) => {
- queryClient.invalidateQueries({ queryKey: mcpQueryKeys.connections() })
-
- // Handle OAuth authorization if needed
- if (result.authorizationUrl) {
- await handleOAuthAuthorization(result.authorizationUrl)
- } else {
- toast.success(t("integration.mcp.service.updated"))
- }
- },
- onError: (error) => {
- toast.error("Failed to update MCP connection")
- console.error("Failed to update MCP connection:", error)
- },
- })
-
- // Mutation for deleting MCP connection
- const deleteConnectionMutation = useMutation({
- mutationFn: deleteMCPConnection,
- onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: mcpQueryKeys.connections() })
- toast.success(t("integration.mcp.service.deleted"))
- },
- onError: (error) => {
- toast.error("Failed to delete MCP connection")
- console.error("Failed to delete MCP connection:", error)
- },
- })
-
- // Mutation for refreshing MCP tools
- const refreshToolsMutation = useMutation({
- mutationFn: (connectionIds?: string[]) => refreshMCPTools(connectionIds),
- onSuccess: () => {
- // Invalidate both connections (for updated counts) and tools queries
- queryClient.invalidateQueries({ queryKey: mcpQueryKeys.connections() })
- queryClient.invalidateQueries({ queryKey: mcpQueryKeys.all })
- toast.success("MCP tools refreshed successfully")
- },
- onError: (error) => {
- toast.error("Failed to refresh MCP tools")
- console.error("Failed to refresh MCP tools:", error)
- },
- })
-
- const { present } = useModalStack()
- const handleAddService = () => {
- present({
- title: "Add MCP Service",
- content: ({ dismiss }: { dismiss: () => void }) => (
- {
- createConnectionMutation.mutate(service)
- dismiss()
- }}
- onCancel={dismiss}
- isLoading={createConnectionMutation.isPending}
- />
- ),
- })
- }
-
- const handleEditService = (service: MCPService) => {
- present({
- title: "Edit MCP Service",
- content: ({ dismiss }: { dismiss: () => void }) => (
- {
- updateConnectionMutation.mutate({
- connectionId: service.id,
- updateData: updatedService,
- })
- dismiss()
- }}
- onCancel={dismiss}
- isLoading={updateConnectionMutation.isPending}
- />
- ),
- })
- }
-
- const handleDeleteService = (id: string) => {
- deleteConnectionMutation.mutate(id)
- }
-
- const handleRefreshTools = (connectionId?: string) => {
- refreshToolsMutation.mutate(connectionId ? [connectionId] : undefined)
- }
-
- // Show error message if query failed
- React.useEffect(() => {
- if (error) {
- toast.error("Failed to load MCP connections")
- console.error("Failed to load MCP connections:", error)
- }
- }, [error])
-
- return (
-
-
-
-
-
-
{t("integration.mcp.description")}
-
-
-
-
-
- {mcpEnabled && (
-
-
-
-
-
-
-
-
-
- {mcpServices.length === 0 && (
-
-
-
-
-
- {t("integration.mcp.services.empty.title")}
-
-
- {t("integration.mcp.services.empty.description")}
-
-
- )}
-
- {isLoading && (
-
-
-
- )}
-
- {mcpServices.map((service) => (
-
- ))}
-
- )}
-
- )
-}
-
-interface MCPServiceItemProps {
- service: MCPService
- onDelete: (id: string) => void
- onRefresh: (connectionId: string) => void
- onEdit: (service: MCPService) => void
- isDeleting?: boolean
- isRefreshing?: boolean
-}
-
-const MCPServiceItem = ({
- service,
- onDelete,
- onRefresh,
- onEdit,
- isDeleting = false,
- isRefreshing = false,
-}: MCPServiceItemProps) => {
- const { t } = useTranslation("ai")
-
- const getConnectionStatusColor = (isConnected: boolean) => {
- return isConnected ? "bg-green/10 text-green" : "bg-gray/10 text-text-tertiary"
- }
-
- const getConnectionStatusText = (isConnected: boolean) => {
- return isConnected
- ? t("integration.mcp.service.connected")
- : t("integration.mcp.service.disconnected")
- }
-
- const formatDate = (dateString: string | null) => {
- if (!dateString) return "Never"
- return new Date(dateString).toLocaleDateString()
- }
-
- return (
-
-
-
-
-
{service.name}
-
- {getConnectionStatusText(service.isConnected)}
-
-
- {service.transportType}
-
-
-
- {service.url && (
-
- URL: {service.url}
-
- )}
-
-
- Tools: {service.toolCount}
- Created:{" "}
- {formatDate(service.createdAt)}
- Last Used:{" "}
- {formatDate(service.lastUsed)}
-
- {service.lastError && (
-
- Error: {service.lastError}
-
- )}
-
-
-
-
-
-
-
-
-
-
- )
-}
-
-interface MCPServiceModalContentProps {
- service?: MCPService | null
- onSave: (service: {
- name: string
- transportType: "streamable-http" | "sse"
- url: string
- headers?: Record
- }) => void
- onCancel: () => void
- isLoading?: boolean
-}
-
-const MCPServiceModalContent = ({
- service,
- onSave,
- onCancel,
- isLoading = false,
-}: MCPServiceModalContentProps) => {
- const { t } = useTranslation("ai")
- const [name, setName] = useState(service?.name || "")
- const [url, setUrl] = useState(service?.url || "")
- const [transportType, setTransportType] = useState<"streamable-http" | "sse">(
- service?.transportType || "streamable-http",
- )
- const [headers, setHeaders] = useState>(service?.headers || {})
-
- const handleSave = () => {
- if (!name.trim()) {
- toast.error(t("integration.mcp.service.validation.name_required"))
- return
- }
-
- if (!url.trim()) {
- toast.error(t("integration.mcp.service.validation.baseUrl_required"))
- return
- }
-
- // Basic URL validation
- try {
- new URL(url.trim())
- } catch {
- toast.error(t("integration.mcp.service.validation.invalid_url"))
- return
- }
-
- onSave({
- name: name.trim(),
- transportType,
- url: url.trim(),
- headers: Object.keys(headers).length > 0 ? headers : undefined,
- })
- }
-
- return (
-
-
-
-
-
- setName(e.target.value)}
- placeholder={t("integration.mcp.service.name_placeholder")}
- />
-
-
-
-
-
-
-
-
-
- setUrl(e.target.value)}
- placeholder="https://example.com/mcp"
- />
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- )
-}
-
-export const PanelStyleSegment = () => {
- const { t } = useTranslation("ai")
- const panelStyle = useAIChatPanelStyle()
-
- return (
- ,
- },
- {
- value: AIChatPanelStyle.Floating,
- label: t("settings.panel_style.floating"),
- icon: ,
- },
- ]}
- onValueChanged={(value) => {
- setAIChatPanelStyle(value as AIChatPanelStyle)
- }}
- />
- )
-}
diff --git a/apps/desktop/layer/renderer/src/modules/settings/tabs/ai/PanelStyleSection.tsx b/apps/desktop/layer/renderer/src/modules/settings/tabs/ai/PanelStyleSection.tsx
new file mode 100644
index 000000000..d093a81ca
--- /dev/null
+++ b/apps/desktop/layer/renderer/src/modules/settings/tabs/ai/PanelStyleSection.tsx
@@ -0,0 +1,34 @@
+import { useTranslation } from "react-i18next"
+
+import { AIChatPanelStyle, setAIChatPanelStyle, useAIChatPanelStyle } from "~/atoms/settings/ai"
+
+import { SettingTabbedSegment } from "../../control"
+
+export const PanelStyleSection = () => {
+ const { t } = useTranslation("ai")
+ const panelStyle = useAIChatPanelStyle()
+
+ return (
+ ,
+ },
+ {
+ value: AIChatPanelStyle.Floating,
+ label: t("settings.panel_style.floating"),
+ icon: ,
+ },
+ ]}
+ onValueChanged={(value) => {
+ setAIChatPanelStyle(value as AIChatPanelStyle)
+ }}
+ />
+ )
+}
diff --git a/apps/desktop/layer/renderer/src/modules/settings/tabs/ai/PersonalizePromptSection.tsx b/apps/desktop/layer/renderer/src/modules/settings/tabs/ai/PersonalizePromptSection.tsx
new file mode 100644
index 000000000..f21950d7a
--- /dev/null
+++ b/apps/desktop/layer/renderer/src/modules/settings/tabs/ai/PersonalizePromptSection.tsx
@@ -0,0 +1,111 @@
+import { Spring } from "@follow/components/constants/spring.js"
+import { Button } from "@follow/components/ui/button/index.js"
+import { TextArea } from "@follow/components/ui/input/index.js"
+import { Label } from "@follow/components/ui/label/index.jsx"
+import { AnimatePresence } from "motion/react"
+import * as React from "react"
+import { useState } from "react"
+import { useTranslation } from "react-i18next"
+import { toast } from "sonner"
+
+import { setAISetting, useAISettingValue } from "~/atoms/settings/ai"
+import { m } from "~/components/common/Motion"
+
+import { SettingDescription } from "../../control"
+import { SettingModalContentPortal } from "../../modal/layout"
+
+export const PersonalizePromptSection = () => {
+ const { t } = useTranslation("ai")
+ const aiSettings = useAISettingValue()
+ const [prompt, setPrompt] = useState(aiSettings.personalizePrompt)
+ const [isSaving, setIsSaving] = useState(false)
+
+ const MAX_CHARACTERS = 500
+ const currentLength = prompt.length
+ const isOverLimit = currentLength > MAX_CHARACTERS
+ const hasChanges = prompt !== aiSettings.personalizePrompt
+
+ const handlePromptChange = (e: React.ChangeEvent) => {
+ const { value } = e.target
+ // Allow typing but show validation error if over limit
+ setPrompt(value)
+ }
+
+ const handleSave = async () => {
+ if (isOverLimit) {
+ toast.error(`Prompt must be ${MAX_CHARACTERS} characters or less`)
+ return
+ }
+
+ setIsSaving(true)
+ try {
+ setAISetting("personalizePrompt", prompt)
+ toast.success(t("personalize.saved"))
+ } finally {
+ setIsSaving(false)
+ }
+ }
+
+ return (
+
+
+
+
+
+
MAX_CHARACTERS * 0.8
+ ? "text-yellow"
+ : "text-text-tertiary"
+ }`}
+ >
+ {currentLength}/{MAX_CHARACTERS}
+
+
+
+ {t("personalize.prompt.help")}
+ {isOverLimit && (
+
+ Prompt exceeds {MAX_CHARACTERS} character limit
+
+ )}
+
+
+
+
+ {hasChanges && (
+
+
+
+ Unsaved changes
+
+
+
+
+ )}
+
+
+ )
+}
diff --git a/apps/desktop/layer/renderer/src/modules/settings/tabs/ai/TokenUsageSection.tsx b/apps/desktop/layer/renderer/src/modules/settings/tabs/ai/TokenUsageSection.tsx
new file mode 100644
index 000000000..bd64b9ed2
--- /dev/null
+++ b/apps/desktop/layer/renderer/src/modules/settings/tabs/ai/TokenUsageSection.tsx
@@ -0,0 +1,84 @@
+import { Progress } from "@follow/components/ui/progress/index.jsx"
+import { useQuery } from "@tanstack/react-query"
+import { useTranslation } from "react-i18next"
+
+import { apiFetch } from "~/lib/api-fetch"
+
+import { SettingDescription } from "../../control"
+
+const useTokenUsage = () => {
+ return useQuery({
+ queryKey: ["aiTokenUsage"],
+ queryFn: async () => {
+ // TODO: replace with api client call
+ return (
+ (await apiFetch("/ai/usage")) as {
+ code: 0
+ data: {
+ total: number
+ used: number
+ remaining: number
+ resetAt: string
+ }
+ }
+ ).data
+ },
+ })
+}
+
+export const TokenUsageSection = () => {
+ const { t } = useTranslation("ai")
+
+ const tokenUsage = useTokenUsage().data || {
+ total: 0,
+ used: 0,
+ remaining: 0,
+ resetAt: new Date(),
+ }
+
+ const usagePercentage = tokenUsage.total === 0 ? 0 : (tokenUsage.used / tokenUsage.total) * 100
+ const resetDate = new Date(tokenUsage.resetAt)
+
+ return (
+
+
{t("token_usage.description")}
+
+
+
+
+
+ {t("token_usage.tokens_used", {
+ used: tokenUsage.used.toLocaleString(),
+ total: tokenUsage.total.toLocaleString(),
+ })}
+
+
+ {tokenUsage.remaining.toLocaleString()} {t("token_usage.tokens_remaining")}
+
+
+
+
+
{Math.round(usagePercentage)}%
+
+
+
+
+
+
+
+ 0
+
+ {t("token_usage.resets_at")}: {resetDate.toLocaleDateString()}
+
+ {tokenUsage.total.toLocaleString()}
+
+
+
+
+
+
+ )
+}
diff --git a/apps/desktop/layer/renderer/src/modules/settings/tabs/ai/index.ts b/apps/desktop/layer/renderer/src/modules/settings/tabs/ai/index.ts
new file mode 100644
index 000000000..82dfe67dc
--- /dev/null
+++ b/apps/desktop/layer/renderer/src/modules/settings/tabs/ai/index.ts
@@ -0,0 +1,5 @@
+export { MCPServicesSection } from "./mcp"
+export { PanelStyleSection } from "./PanelStyleSection"
+export { PersonalizePromptSection } from "./PersonalizePromptSection"
+export { AIShortcutsSection } from "./shortcuts"
+export { TokenUsageSection } from "./TokenUsageSection"
diff --git a/apps/desktop/layer/renderer/src/modules/settings/tabs/ai/mcp/MCPServiceItem.tsx b/apps/desktop/layer/renderer/src/modules/settings/tabs/ai/mcp/MCPServiceItem.tsx
new file mode 100644
index 000000000..eafc48863
--- /dev/null
+++ b/apps/desktop/layer/renderer/src/modules/settings/tabs/ai/mcp/MCPServiceItem.tsx
@@ -0,0 +1,109 @@
+import { Button } from "@follow/components/ui/button/index.js"
+import type { MCPService } from "@follow/shared/settings/interface"
+import { useTranslation } from "react-i18next"
+
+interface MCPServiceItemProps {
+ service: MCPService
+ onDelete: (id: string) => void
+ onRefresh: (connectionId: string) => void
+ onEdit: (service: MCPService) => void
+ isDeleting?: boolean
+ isRefreshing?: boolean
+}
+
+export const MCPServiceItem = ({
+ service,
+ onDelete,
+ onRefresh,
+ onEdit,
+ isDeleting = false,
+ isRefreshing = false,
+}: MCPServiceItemProps) => {
+ const { t } = useTranslation("ai")
+
+ const getConnectionStatusColor = (isConnected: boolean) => {
+ return isConnected ? "bg-green/10 text-green" : "bg-gray/10 text-text-tertiary"
+ }
+
+ const getConnectionStatusText = (isConnected: boolean) => {
+ return isConnected
+ ? t("integration.mcp.service.connected")
+ : t("integration.mcp.service.disconnected")
+ }
+
+ const formatDate = (dateString: string | null) => {
+ if (!dateString) return "Never"
+ return new Date(dateString).toLocaleDateString()
+ }
+
+ return (
+
+
+
+
+
{service.name}
+
+ {getConnectionStatusText(service.isConnected)}
+
+
+ {service.transportType}
+
+
+
+ {service.url && (
+
+ URL: {service.url}
+
+ )}
+
+
+ Tools: {service.toolCount}
+ Created:{" "}
+ {formatDate(service.createdAt)}
+ Last Used:{" "}
+ {formatDate(service.lastUsed)}
+
+ {service.lastError && (
+
+ Error: {service.lastError}
+
+ )}
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/apps/desktop/layer/renderer/src/modules/settings/tabs/ai/mcp/MCPServiceModalContent.tsx b/apps/desktop/layer/renderer/src/modules/settings/tabs/ai/mcp/MCPServiceModalContent.tsx
new file mode 100644
index 000000000..f778066d6
--- /dev/null
+++ b/apps/desktop/layer/renderer/src/modules/settings/tabs/ai/mcp/MCPServiceModalContent.tsx
@@ -0,0 +1,142 @@
+import { Button } from "@follow/components/ui/button/index.js"
+import { Input } from "@follow/components/ui/input/index.js"
+import { KeyValueEditor } from "@follow/components/ui/key-value-editor/index.js"
+import { Label } from "@follow/components/ui/label/index.jsx"
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@follow/components/ui/select/index.js"
+import type { MCPService } from "@follow/shared/settings/interface"
+import { useState } from "react"
+import { useTranslation } from "react-i18next"
+import { toast } from "sonner"
+
+interface MCPServiceModalContentProps {
+ service?: MCPService | null
+ onSave: (service: {
+ name: string
+ transportType: "streamable-http" | "sse"
+ url: string
+ headers?: Record
+ }) => void
+ onCancel: () => void
+ isLoading?: boolean
+}
+
+export const MCPServiceModalContent = ({
+ service,
+ onSave,
+ onCancel,
+ isLoading = false,
+}: MCPServiceModalContentProps) => {
+ const { t } = useTranslation("ai")
+ const [name, setName] = useState(service?.name || "")
+ const [url, setUrl] = useState(service?.url || "")
+ const [transportType, setTransportType] = useState<"streamable-http" | "sse">(
+ service?.transportType || "streamable-http",
+ )
+ const [headers, setHeaders] = useState>(service?.headers || {})
+
+ const handleSave = () => {
+ if (!name.trim()) {
+ toast.error(t("integration.mcp.service.validation.name_required"))
+ return
+ }
+
+ if (!url.trim()) {
+ toast.error(t("integration.mcp.service.validation.baseUrl_required"))
+ return
+ }
+
+ // Basic URL validation
+ try {
+ new URL(url.trim())
+ } catch {
+ toast.error(t("integration.mcp.service.validation.invalid_url"))
+ return
+ }
+
+ onSave({
+ name: name.trim(),
+ transportType,
+ url: url.trim(),
+ headers: Object.keys(headers).length > 0 ? headers : undefined,
+ })
+ }
+
+ return (
+
+
+
+
+
+ setName(e.target.value)}
+ placeholder={t("integration.mcp.service.name_placeholder")}
+ />
+
+
+
+
+
+
+
+
+
+ setUrl(e.target.value)}
+ placeholder="https://example.com/mcp"
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/apps/desktop/layer/renderer/src/modules/settings/tabs/ai/mcp/MCPServicesSection.tsx b/apps/desktop/layer/renderer/src/modules/settings/tabs/ai/mcp/MCPServicesSection.tsx
new file mode 100644
index 000000000..1a103e138
--- /dev/null
+++ b/apps/desktop/layer/renderer/src/modules/settings/tabs/ai/mcp/MCPServicesSection.tsx
@@ -0,0 +1,282 @@
+import { Button } from "@follow/components/ui/button/index.js"
+import { Label } from "@follow/components/ui/label/index.jsx"
+import { Switch } from "@follow/components/ui/switch/index.jsx"
+import type { MCPService } from "@follow/shared/settings/interface"
+import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
+import * as React from "react"
+import { useTranslation } from "react-i18next"
+import { toast } from "sonner"
+
+import { setMCPEnabled, useMCPEnabled } from "~/atoms/settings/ai"
+import { useDialog, useModalStack } from "~/components/ui/modal/stacked/hooks"
+import {
+ createMCPConnection,
+ deleteMCPConnection,
+ fetchMCPConnections,
+ mcpQueryKeys,
+ refreshMCPTools,
+ updateMCPConnection,
+} from "~/queries/mcp"
+
+import { MCPServiceItem } from "./MCPServiceItem"
+import { MCPServiceModalContent } from "./MCPServiceModalContent"
+
+export const MCPServicesSection = () => {
+ const { t } = useTranslation("ai")
+ const mcpEnabled = useMCPEnabled()
+ const queryClient = useQueryClient()
+ const dialog = useDialog()
+
+ // Reusable OAuth authorization handler using dialog
+ const handleOAuthAuthorization = async (authorizationUrl: string, connectionId?: string) => {
+ const confirmed = await dialog.ask({
+ title: t("integration.mcp.service.auth_required"),
+ message: t("integration.mcp.service.auth_message"),
+ confirmText: t("integration.mcp.service.open_auth"),
+ cancelText: t("words.cancel", { ns: "common" }),
+ variant: "ask",
+ })
+
+ if (confirmed) {
+ const popup = window.open(
+ authorizationUrl,
+ "_blank",
+ "width=600,height=700,scrollbars=yes,resizable=yes",
+ )
+
+ if (!popup) {
+ toast.error(t("integration.mcp.service.popup_blocked"))
+ } else {
+ popup.onclose = () => {
+ // FIXME
+ setTimeout(() => {
+ refreshToolsMutation.mutate(connectionId ? [connectionId] : undefined)
+ }, 3000)
+ }
+ }
+ }
+ }
+
+ // Query for MCP connections
+ const {
+ data: mcpServices = [],
+ isLoading,
+ error,
+ refetch,
+ } = useQuery({
+ queryKey: mcpQueryKeys.connections(),
+ queryFn: fetchMCPConnections,
+ enabled: mcpEnabled,
+ refetchInterval: 30_000,
+ refetchOnWindowFocus: true,
+ retry: 2,
+ })
+
+ // Mutation for creating MCP connection
+ const createConnectionMutation = useMutation({
+ mutationFn: createMCPConnection,
+ onSuccess: async (result) => {
+ queryClient.invalidateQueries({ queryKey: mcpQueryKeys.connections() })
+
+ // Handle OAuth authorization if needed
+ if (result.authorizationUrl) {
+ await handleOAuthAuthorization(result.authorizationUrl, result.connectionId)
+ } else {
+ toast.success(t("integration.mcp.service.added"))
+ refreshToolsMutation.mutate([result.connectionId])
+ }
+ },
+ onError: (error) => {
+ toast.error(t("integration.mcp.service.discovery_failed"))
+ console.error("Failed to create MCP connection:", error)
+ },
+ })
+
+ // Mutation for updating MCP connection
+ const updateConnectionMutation = useMutation({
+ mutationFn: ({
+ connectionId,
+ updateData,
+ }: {
+ connectionId: string
+ updateData: Parameters[1]
+ }) => updateMCPConnection(connectionId, updateData),
+ onSuccess: async (result) => {
+ queryClient.invalidateQueries({ queryKey: mcpQueryKeys.connections() })
+
+ // Handle OAuth authorization if needed
+ if (result.authorizationUrl) {
+ await handleOAuthAuthorization(result.authorizationUrl, result.connectionId)
+ } else {
+ toast.success(t("integration.mcp.service.updated"))
+ refreshToolsMutation.mutate([result.connectionId])
+ }
+ },
+ onError: (error) => {
+ toast.error("Failed to update MCP connection")
+ console.error("Failed to update MCP connection:", error)
+ },
+ })
+
+ // Mutation for deleting MCP connection
+ const deleteConnectionMutation = useMutation({
+ mutationFn: deleteMCPConnection,
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: mcpQueryKeys.connections() })
+ toast.success(t("integration.mcp.service.deleted"))
+ },
+ onError: (error) => {
+ toast.error("Failed to delete MCP connection")
+ console.error("Failed to delete MCP connection:", error)
+ },
+ })
+
+ // Mutation for refreshing MCP tools
+ const refreshToolsMutation = useMutation({
+ mutationFn: (connectionIds?: string[]) => refreshMCPTools(connectionIds),
+ onSuccess: () => {
+ // Invalidate both connections (for updated counts) and tools queries
+ queryClient.invalidateQueries({ queryKey: mcpQueryKeys.connections() })
+ queryClient.invalidateQueries({ queryKey: mcpQueryKeys.all })
+ toast.success("MCP tools refreshed successfully")
+ },
+ onError: (error) => {
+ toast.error("Failed to refresh MCP tools")
+ console.error("Failed to refresh MCP tools:", error)
+ },
+ })
+
+ const { present } = useModalStack()
+ const handleAddService = () => {
+ present({
+ title: "Add MCP Service",
+ content: ({ dismiss }: { dismiss: () => void }) => (
+ {
+ createConnectionMutation.mutate(service)
+ dismiss()
+ }}
+ onCancel={dismiss}
+ isLoading={createConnectionMutation.isPending}
+ />
+ ),
+ })
+ }
+
+ const handleEditService = (service: MCPService) => {
+ present({
+ title: "Edit MCP Service",
+ content: ({ dismiss }: { dismiss: () => void }) => (
+ {
+ updateConnectionMutation.mutate({
+ connectionId: service.id,
+ updateData: updatedService,
+ })
+ dismiss()
+ }}
+ onCancel={dismiss}
+ isLoading={updateConnectionMutation.isPending}
+ />
+ ),
+ })
+ }
+
+ const handleDeleteService = (id: string) => {
+ deleteConnectionMutation.mutate(id)
+ }
+
+ const handleRefreshTools = (connectionId?: string) => {
+ refreshToolsMutation.mutate(connectionId ? [connectionId] : undefined)
+ }
+
+ // Show error message if query failed
+ React.useEffect(() => {
+ if (error) {
+ toast.error("Failed to load MCP connections")
+ console.error("Failed to load MCP connections:", error)
+ }
+ }, [error])
+
+ return (
+
+
+
+
+
+
{t("integration.mcp.description")}
+
+
+
+
+
+ {mcpEnabled && (
+
+
+
+
+
+
+
+
+
+ {mcpServices.length === 0 && (
+
+
+
+
+
+ {t("integration.mcp.services.empty.title")}
+
+
+ {t("integration.mcp.services.empty.description")}
+
+
+ )}
+
+ {isLoading && (
+
+
+
+ )}
+
+ {mcpServices.map((service) => (
+
+ ))}
+
+ )}
+
+ )
+}
diff --git a/apps/desktop/layer/renderer/src/modules/settings/tabs/ai/mcp/index.ts b/apps/desktop/layer/renderer/src/modules/settings/tabs/ai/mcp/index.ts
new file mode 100644
index 000000000..e4237d91a
--- /dev/null
+++ b/apps/desktop/layer/renderer/src/modules/settings/tabs/ai/mcp/index.ts
@@ -0,0 +1,3 @@
+export { MCPServiceItem } from "./MCPServiceItem"
+export { MCPServiceModalContent } from "./MCPServiceModalContent"
+export { MCPServicesSection } from "./MCPServicesSection"
diff --git a/apps/desktop/layer/renderer/src/modules/settings/tabs/ai/shortcuts/AIShortcutsSection.tsx b/apps/desktop/layer/renderer/src/modules/settings/tabs/ai/shortcuts/AIShortcutsSection.tsx
new file mode 100644
index 000000000..db3d4cf55
--- /dev/null
+++ b/apps/desktop/layer/renderer/src/modules/settings/tabs/ai/shortcuts/AIShortcutsSection.tsx
@@ -0,0 +1,104 @@
+import type { AIShortcut } from "@follow/shared/settings/interface"
+import { useTranslation } from "react-i18next"
+import { toast } from "sonner"
+
+import { setAISetting, useAISettingValue } from "~/atoms/settings/ai"
+import { useModalStack } from "~/components/ui/modal/stacked/hooks"
+
+import { SettingActionItem } from "../../../control"
+import { ShortcutItem } from "./ShortcutItem"
+import { ShortcutModalContent } from "./ShortcutModalContent"
+
+export const AIShortcutsSection = () => {
+ const { t } = useTranslation("ai")
+ const { shortcuts } = useAISettingValue()
+ const { present } = useModalStack()
+
+ const handleAddShortcut = () => {
+ present({
+ title: "Add AI Shortcut",
+ content: ({ dismiss }: { dismiss: () => void }) => (
+ {
+ const newShortcut: AIShortcut = {
+ ...shortcut,
+ id: Date.now().toString(),
+ }
+ setAISetting("shortcuts", [...shortcuts, newShortcut])
+ toast.success(t("shortcuts.added"))
+ dismiss()
+ }}
+ onCancel={dismiss}
+ />
+ ),
+ })
+ }
+
+ const handleEditShortcut = (shortcut: AIShortcut) => {
+ present({
+ title: "Edit AI Shortcut",
+ content: ({ dismiss }: { dismiss: () => void }) => (
+ {
+ setAISetting(
+ "shortcuts",
+ shortcuts.map((s) =>
+ s.id === shortcut.id ? { ...updatedShortcut, id: shortcut.id } : s,
+ ),
+ )
+ toast.success(t("shortcuts.updated"))
+ dismiss()
+ }}
+ onCancel={dismiss}
+ />
+ ),
+ })
+ }
+
+ const handleDeleteShortcut = (id: string) => {
+ setAISetting(
+ "shortcuts",
+ shortcuts.filter((s) => s.id !== id),
+ )
+ toast.success(t("shortcuts.deleted"))
+ }
+
+ const handleToggleShortcut = (id: string, enabled: boolean) => {
+ setAISetting(
+ "shortcuts",
+ shortcuts.map((s) => (s.id === id ? { ...s, enabled } : s)),
+ )
+ }
+
+ return (
+
+
+
+ {shortcuts.length === 0 && (
+
+
+
+
+
{t("shortcuts.empty.title")}
+
{t("shortcuts.empty.description")}
+
+ )}
+
+ {shortcuts.map((shortcut) => (
+
+ ))}
+
+ )
+}
diff --git a/apps/desktop/layer/renderer/src/modules/settings/tabs/ai/shortcuts/ShortcutItem.tsx b/apps/desktop/layer/renderer/src/modules/settings/tabs/ai/shortcuts/ShortcutItem.tsx
new file mode 100644
index 000000000..977b99f95
--- /dev/null
+++ b/apps/desktop/layer/renderer/src/modules/settings/tabs/ai/shortcuts/ShortcutItem.tsx
@@ -0,0 +1,54 @@
+import { Button } from "@follow/components/ui/button/index.js"
+import { KbdCombined } from "@follow/components/ui/kbd/Kbd.js"
+import { Switch } from "@follow/components/ui/switch/index.jsx"
+import type { AIShortcut } from "@follow/shared/settings/interface"
+
+interface ShortcutItemProps {
+ shortcut: AIShortcut
+ onDelete: (id: string) => void
+ onToggle: (id: string, enabled: boolean) => void
+ onEdit: (shortcut: AIShortcut) => void
+}
+
+export const ShortcutItem = ({ shortcut, onDelete, onToggle, onEdit }: ShortcutItemProps) => {
+ return (
+
+
+
+
+
{shortcut.name}
+ {shortcut.hotkey && (
+
+ {shortcut.hotkey}
+
+ )}
+
+
+ {shortcut.prompt}
+
+
+
+
+
+
+
+
+
+
+
+ {shortcut.enabled ? "ON" : "OFF"}
+
+ onToggle(shortcut.id, enabled)}
+ />
+
+
+
+
+ )
+}
diff --git a/apps/desktop/layer/renderer/src/modules/settings/tabs/ai/shortcuts/ShortcutModalContent.tsx b/apps/desktop/layer/renderer/src/modules/settings/tabs/ai/shortcuts/ShortcutModalContent.tsx
new file mode 100644
index 000000000..e2f215b57
--- /dev/null
+++ b/apps/desktop/layer/renderer/src/modules/settings/tabs/ai/shortcuts/ShortcutModalContent.tsx
@@ -0,0 +1,105 @@
+import { Button } from "@follow/components/ui/button/index.js"
+import { Input, TextArea } from "@follow/components/ui/input/index.js"
+import { Label } from "@follow/components/ui/label/index.jsx"
+import { Switch } from "@follow/components/ui/switch/index.jsx"
+import type { AIShortcut } from "@follow/shared/settings/interface"
+import { useState } from "react"
+import { useTranslation } from "react-i18next"
+import { toast } from "sonner"
+
+interface ShortcutModalContentProps {
+ shortcut?: AIShortcut | null
+ onSave: (shortcut: Omit) => void
+ onCancel: () => void
+}
+
+export const ShortcutModalContent = ({ shortcut, onSave, onCancel }: ShortcutModalContentProps) => {
+ const { t } = useTranslation("ai")
+ const [name, setName] = useState(shortcut?.name || "")
+ const [prompt, setPrompt] = useState(shortcut?.prompt || "")
+ const [enabled, setEnabled] = useState(shortcut?.enabled ?? true)
+
+ const handleSave = () => {
+ if (!name.trim() || !prompt.trim()) {
+ toast.error(t("shortcuts.validation.required"))
+ return
+ }
+
+ onSave({
+ name: name.trim(),
+ prompt: prompt.trim(),
+ enabled,
+ })
+ }
+
+ return (
+
+
+
+
+ setName(e.target.value)}
+ placeholder={t("shortcuts.name_placeholder")}
+ />
+
+ {/*
+
+
+
*/}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/apps/desktop/layer/renderer/src/modules/settings/tabs/ai/shortcuts/index.ts b/apps/desktop/layer/renderer/src/modules/settings/tabs/ai/shortcuts/index.ts
new file mode 100644
index 000000000..e58013a99
--- /dev/null
+++ b/apps/desktop/layer/renderer/src/modules/settings/tabs/ai/shortcuts/index.ts
@@ -0,0 +1,3 @@
+export { AIShortcutsSection } from "./AIShortcutsSection"
+export { ShortcutItem } from "./ShortcutItem"
+export { ShortcutModalContent } from "./ShortcutModalContent"
diff --git a/apps/desktop/layer/renderer/src/queries/mcp.ts b/apps/desktop/layer/renderer/src/queries/mcp.ts
index a896195c1..6c755d267 100644
--- a/apps/desktop/layer/renderer/src/queries/mcp.ts
+++ b/apps/desktop/layer/renderer/src/queries/mcp.ts
@@ -2,15 +2,13 @@ import type { MCPService } from "@follow/shared/settings/interface"
import { followApi } from "~/lib/api-client"
-// MCP Service API calls - these connect to the actual server endpoints via followClient
export const createMCPConnection = async (connectionData: {
name: string
transportType: "streamable-http" | "sse"
url: string
headers?: Record
-}): Promise<{ authorizationUrl?: string }> => {
- const res = await followApi.mcp.createConnection(connectionData)
- return { authorizationUrl: res.authorizationUrl }
+}) => {
+ return followApi.mcp.createConnection(connectionData)
}
export const fetchMCPConnections = async (): Promise => {
@@ -26,9 +24,8 @@ export const updateMCPConnection = async (
url?: string
headers?: Record
},
-): Promise<{ authorizationUrl?: string }> => {
- const res = await followApi.mcp.updateConnection({ connectionId, ...updateData })
- return { authorizationUrl: res.authorizationUrl }
+) => {
+ return followApi.mcp.updateConnection({ connectionId, ...updateData })
}
export const deleteMCPConnection = async (connectionId: string): Promise => {
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index a7d522d83..9d73f13bc 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -7,8 +7,8 @@ settings:
catalogs:
default:
'@follow-app/client-sdk':
- specifier: 0.3.39
- version: 0.3.39
+ specifier: 0.3.40
+ version: 0.3.40
tailwindcss-uikit-colors:
specifier: 1.0.0
version: 1.0.0
@@ -465,7 +465,7 @@ importers:
version: 3.0.2(electron@37.2.0)
'@follow-app/client-sdk':
specifier: 'catalog:'
- version: 0.3.39(@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)
+ version: 0.3.40(@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)
'@follow/database':
specifier: workspace:*
version: link:../../../../packages/internal/database
@@ -1673,7 +1673,7 @@ importers:
dependencies:
'@follow-app/client-sdk':
specifier: 'catalog:'
- version: 0.3.39(@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)
+ version: 0.3.40(@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)
'@follow/configs':
specifier: workspace:*
version: link:../../configs
@@ -1685,7 +1685,7 @@ importers:
dependencies:
'@follow-app/client-sdk':
specifier: 'catalog:'
- version: 0.3.39(@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)
+ version: 0.3.40(@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)
'@follow/constants':
specifier: workspace:*
version: link:../constants
@@ -1763,7 +1763,7 @@ importers:
dependencies:
'@follow-app/client-sdk':
specifier: 'catalog:'
- version: 0.3.39(@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)
+ version: 0.3.40(@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)
'@follow/constants':
specifier: workspace:*
version: link:../constants
@@ -1827,7 +1827,7 @@ importers:
dependencies:
'@follow-app/client-sdk':
specifier: 'catalog:'
- version: 0.3.39(@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)
+ version: 0.3.40(@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)
'@follow/configs':
specifier: workspace:*
version: link:../../configs
@@ -4051,20 +4051,20 @@ packages:
'@floating-ui/utils@0.2.10':
resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==}
- '@follow-app/client-sdk@0.3.39':
- resolution: {integrity: sha512-NnjLOzrQ2UD6LgwH6ItK0SJts5UinzLCXJFJf74X096a6qd9xhRuHJ3X+Ea3qNw/6WXcK4l3dnkHBHDaDfthRA==}
+ '@follow-app/client-sdk@0.3.40':
+ resolution: {integrity: sha512-AsSZlh10NvmskdVU1XuUh3S2rVBmz0qoiMRUtdbdU5/JARtwNM1UiIo1WfaGpinht/ZVsKLsR40p03XovRynmQ==}
'@folo-services/ai-tools@0.2.20':
resolution: {integrity: sha512-1AJI48H2g64JhuTruSqQKUrn4Tc353hMFdKAbohh65AxTAMN/1pIw9w17Dtjm+g5BVlZUXYKaiIvaN9tHsXLNA==}
- '@folo-services/constants@0.1.22':
- resolution: {integrity: sha512-zF3yX7lxTpAsZQjp8+P56nEIa7IqDVIyqMZcNpFLm8UHTS2ltPeM/S9KdNbacz9lxDmWjjCmZdEfuBbs0B5b8Q==}
+ '@folo-services/constants@0.1.23':
+ resolution: {integrity: sha512-2tK244KD/RhR6KoLarEPHkMoco3/JvAbL05TCr5pM3pEem7hqCzHNLLDFwAdEl2m1ByTTQHnqqhx/rEZx1BSgA==}
'@folo-services/drizzle@0.1.13':
resolution: {integrity: sha512-h6tndGnTLrAmfvxK5XsXGdZaMi2GBmFZBFiBqtabC7Y05A1ZSOaWg/d9f60Ydh586LmVonz5VGrhvzTdn5ukhg==}
- '@folo-services/drizzle@0.1.17':
- resolution: {integrity: sha512-g0BgfW+hn6vmQyz9SSTOV5KJVexgFoyEgEgQCeSVM6KxufH88VxVpJzkGA1zb7CxEuWrxTuHmpBEolcNFfABTQ==}
+ '@folo-services/drizzle@0.1.18':
+ resolution: {integrity: sha512-mCpxdUGpkLJ6YoohpYarkQR0fobCY1ZUZmDqyHxk9p6PtzyD/WziXYUAs9IcCuICCUiIt3dyiPie+lx/Nx3hMw==}
'@folo-services/exceptions@0.1.11':
resolution: {integrity: sha512-OMKat8KcIxjw9pBfG3pC1kRcIs0YiuaYmMDA2MRy8AEGcki8F9u5e5QH7OKgP/LlNR7Fs2Csz1XetvcC4nB+RQ==}
@@ -4072,8 +4072,8 @@ packages:
'@folo-services/exceptions@0.1.14':
resolution: {integrity: sha512-VFVRkzXg7BytEvdg7iyUXc6qaAudTTtOU8PrTkRUG4ZB9udLE/uVT5q+6poS1CkwMi2GroH27AFC4fnfVp6Fvw==}
- '@folo-services/shared@0.0.12':
- resolution: {integrity: sha512-CHBdwFrke6Mbwxu5MWw9tB7q9CICG2Vh/9IZ8BIa1M65sF/IBZi1sE2roHayuDix0TXXJbceaD+Tv84ROFdO3g==}
+ '@folo-services/shared@0.0.13':
+ resolution: {integrity: sha512-bge+IHFzCjr/QonZqQnDudlLc+P8OuimWR0bdRXUgnNjZ9R6jzIxSB5xYqYVY6DnHM+YyIOYnHAIT09Y3la4fw==}
'@fontsource/sn-pro@5.2.5':
resolution: {integrity: sha512-rBdBv/0ygj6bkO7xDMMFpwobLdSrcQ2Jncb6DIwdeYGoAgeWkQRwVYhGDKasfLjEYCNYxrqr6wsXsM9+aU39RA==}
@@ -18933,12 +18933,12 @@ snapshots:
'@floating-ui/utils@0.2.10': {}
- '@follow-app/client-sdk@0.3.39(@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)':
+ '@follow-app/client-sdk@0.3.40(@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:
- '@folo-services/constants': 0.1.22
- '@folo-services/drizzle': 0.1.17(@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)
+ '@folo-services/constants': 0.1.23
+ '@folo-services/drizzle': 0.1.18(@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)
'@folo-services/exceptions': 0.1.14
- '@folo-services/shared': 0.0.12(@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/shared': 0.0.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)(pg@8.16.3)
zod: 3.25.76
transitivePeerDependencies:
- '@aws-sdk/client-rds-data'
@@ -19011,7 +19011,7 @@ snapshots:
- sql.js
- sqlite3
- '@folo-services/constants@0.1.22':
+ '@folo-services/constants@0.1.23':
dependencies:
zod: 3.25.76
@@ -19054,7 +19054,7 @@ snapshots:
- sql.js
- sqlite3
- '@folo-services/drizzle@0.1.17(@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)':
+ '@folo-services/drizzle@0.1.18(@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.14
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)
@@ -19097,9 +19097,9 @@ snapshots:
'@folo-services/exceptions@0.1.14': {}
- '@folo-services/shared@0.0.12(@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/shared@0.0.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)(pg@8.16.3)':
dependencies:
- '@folo-services/drizzle': 0.1.17(@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)
+ '@folo-services/drizzle': 0.1.18(@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)
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)
zod: 3.25.76
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index c2dfcd614..dfb343667 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -9,7 +9,7 @@ packages:
catalog:
typescript: 5.8.3
- "@follow-app/client-sdk": 0.3.39
+ "@follow-app/client-sdk": 0.3.40
tailwindcss-uikit-colors: 1.0.0
onlyBuiltDependencies: