diff --git a/surfsense_backend/alembic/versions/138_add_thread_auto_model_pinning_fields.py b/surfsense_backend/alembic/versions/138_add_thread_auto_model_pinning_fields.py index 1ea549975..3972b84b9 100644 --- a/surfsense_backend/alembic/versions/138_add_thread_auto_model_pinning_fields.py +++ b/surfsense_backend/alembic/versions/138_add_thread_auto_model_pinning_fields.py @@ -47,19 +47,11 @@ def upgrade() -> None: def downgrade() -> None: - op.execute( - "DROP INDEX IF EXISTS ix_new_chat_threads_pinned_auto_mode" - ) - op.execute( - "DROP INDEX IF EXISTS ix_new_chat_threads_pinned_llm_config_id" - ) + op.execute("DROP INDEX IF EXISTS ix_new_chat_threads_pinned_auto_mode") + op.execute("DROP INDEX IF EXISTS ix_new_chat_threads_pinned_llm_config_id") - op.execute( - "ALTER TABLE new_chat_threads DROP COLUMN IF EXISTS pinned_at" - ) - op.execute( - "ALTER TABLE new_chat_threads DROP COLUMN IF EXISTS pinned_auto_mode" - ) + op.execute("ALTER TABLE new_chat_threads DROP COLUMN IF EXISTS pinned_at") + op.execute("ALTER TABLE new_chat_threads DROP COLUMN IF EXISTS pinned_auto_mode") op.execute( "ALTER TABLE new_chat_threads DROP COLUMN IF EXISTS pinned_llm_config_id" ) diff --git a/surfsense_backend/app/services/auto_model_pin_service.py b/surfsense_backend/app/services/auto_model_pin_service.py index 6bdb60f57..6b69c91ea 100644 --- a/surfsense_backend/app/services/auto_model_pin_service.py +++ b/surfsense_backend/app/services/auto_model_pin_service.py @@ -44,7 +44,9 @@ def _is_usable_global_config(cfg: dict) -> bool: def _global_candidates() -> list[dict]: - candidates = [cfg for cfg in config.GLOBAL_LLM_CONFIGS if _is_usable_global_config(cfg)] + candidates = [ + cfg for cfg in config.GLOBAL_LLM_CONFIGS if _is_usable_global_config(cfg) + ] return sorted(candidates, key=lambda c: int(c.get("id", 0))) @@ -69,7 +71,9 @@ def _to_uuid(user_id: str | UUID | None) -> UUID | None: return None -async def _is_premium_eligible(session: AsyncSession, user_id: str | UUID | None) -> bool: +async def _is_premium_eligible( + session: AsyncSession, user_id: str | UUID | None +) -> bool: parsed = _to_uuid(user_id) if parsed is None: return False @@ -136,8 +140,7 @@ async def resolve_or_get_pinned_llm_config_id( pinned_id = thread.pinned_llm_config_id if ( not force_repin_free - and - thread.pinned_auto_mode == AUTO_FASTEST_MODE + and thread.pinned_auto_mode == AUTO_FASTEST_MODE and pinned_id is not None and int(pinned_id) in candidate_by_id ): @@ -163,7 +166,9 @@ async def resolve_or_get_pinned_llm_config_id( thread.pinned_auto_mode, ) - premium_eligible = False if force_repin_free else await _is_premium_eligible(session, user_id) + premium_eligible = ( + False if force_repin_free else await _is_premium_eligible(session, user_id) + ) if premium_eligible: eligible = candidates else: diff --git a/surfsense_backend/app/tasks/chat/stream_new_chat.py b/surfsense_backend/app/tasks/chat/stream_new_chat.py index 63c149771..5abcb63eb 100644 --- a/surfsense_backend/app/tasks/chat/stream_new_chat.py +++ b/surfsense_backend/app/tasks/chat/stream_new_chat.py @@ -2225,9 +2225,7 @@ async def stream_new_chat( # Premium quota reservation for pinned premium model only. _needs_premium_quota = ( - agent_config is not None - and user_id - and agent_config.is_premium + agent_config is not None and user_id and agent_config.is_premium ) if _needs_premium_quota: import uuid as _uuid @@ -2271,7 +2269,9 @@ async def stream_new_chat( yield streaming_service.format_done() return - llm, agent_config, llm_load_error = await _load_llm_bundle(llm_config_id) + llm, agent_config, llm_load_error = await _load_llm_bundle( + llm_config_id + ) if llm_load_error: yield _emit_stream_error( message=llm_load_error, @@ -3086,9 +3086,7 @@ async def stream_resume_chat( _resume_premium_reserved = 0 _resume_premium_request_id: str | None = None _resume_needs_premium = ( - agent_config is not None - and user_id - and agent_config.is_premium + agent_config is not None and user_id and agent_config.is_premium ) if _resume_needs_premium: import uuid as _uuid @@ -3132,7 +3130,9 @@ async def stream_resume_chat( yield streaming_service.format_done() return - llm, agent_config, llm_load_error = await _load_llm_bundle(llm_config_id) + llm, agent_config, llm_load_error = await _load_llm_bundle( + llm_config_id + ) if llm_load_error: yield _emit_stream_error( message=llm_load_error, diff --git a/surfsense_backend/tests/unit/services/test_auto_model_pin_service.py b/surfsense_backend/tests/unit/services/test_auto_model_pin_service.py index f08e50ba2..0a2342e05 100644 --- a/surfsense_backend/tests/unit/services/test_auto_model_pin_service.py +++ b/surfsense_backend/tests/unit/services/test_auto_model_pin_service.py @@ -66,7 +66,13 @@ async def test_auto_first_turn_pins_one_model(monkeypatch): "GLOBAL_LLM_CONFIGS", [ {"id": -2, "provider": "OPENAI", "model_name": "gpt-free", "api_key": "k1"}, - {"id": -1, "provider": "OPENAI", "model_name": "gpt-prem", "api_key": "k2", "billing_tier": "premium"}, + { + "id": -1, + "provider": "OPENAI", + "model_name": "gpt-prem", + "api_key": "k2", + "billing_tier": "premium", + }, ], ) @@ -103,12 +109,20 @@ async def test_next_turn_reuses_existing_pin(monkeypatch): config, "GLOBAL_LLM_CONFIGS", [ - {"id": -1, "provider": "OPENAI", "model_name": "gpt-prem", "api_key": "k2", "billing_tier": "premium"}, + { + "id": -1, + "provider": "OPENAI", + "model_name": "gpt-prem", + "api_key": "k2", + "billing_tier": "premium", + }, ], ) async def _must_not_call(*_args, **_kwargs): - raise AssertionError("premium_get_usage should not be called for valid pin reuse") + raise AssertionError( + "premium_get_usage should not be called for valid pin reuse" + ) monkeypatch.setattr( "app.services.auto_model_pin_service.TokenQuotaService.premium_get_usage", @@ -136,7 +150,13 @@ async def test_premium_eligible_auto_can_pin_premium(monkeypatch): config, "GLOBAL_LLM_CONFIGS", [ - {"id": -1, "provider": "OPENAI", "model_name": "gpt-prem", "api_key": "k2", "billing_tier": "premium"}, + { + "id": -1, + "provider": "OPENAI", + "model_name": "gpt-prem", + "api_key": "k2", + "billing_tier": "premium", + }, ], ) @@ -168,8 +188,20 @@ async def test_premium_ineligible_auto_pins_free_only(monkeypatch): config, "GLOBAL_LLM_CONFIGS", [ - {"id": -2, "provider": "OPENAI", "model_name": "gpt-free", "api_key": "k1", "billing_tier": "free"}, - {"id": -1, "provider": "OPENAI", "model_name": "gpt-prem", "api_key": "k2", "billing_tier": "premium"}, + { + "id": -2, + "provider": "OPENAI", + "model_name": "gpt-free", + "api_key": "k1", + "billing_tier": "free", + }, + { + "id": -1, + "provider": "OPENAI", + "model_name": "gpt-prem", + "api_key": "k2", + "billing_tier": "premium", + }, ], ) @@ -203,8 +235,20 @@ async def test_pinned_premium_stays_premium_after_quota_exhaustion(monkeypatch): config, "GLOBAL_LLM_CONFIGS", [ - {"id": -2, "provider": "OPENAI", "model_name": "gpt-free", "api_key": "k1", "billing_tier": "free"}, - {"id": -1, "provider": "OPENAI", "model_name": "gpt-prem", "api_key": "k2", "billing_tier": "premium"}, + { + "id": -2, + "provider": "OPENAI", + "model_name": "gpt-free", + "api_key": "k1", + "billing_tier": "free", + }, + { + "id": -1, + "provider": "OPENAI", + "model_name": "gpt-prem", + "api_key": "k2", + "billing_tier": "premium", + }, ], ) @@ -238,8 +282,20 @@ async def test_force_repin_free_switches_auto_premium_pin_to_free(monkeypatch): config, "GLOBAL_LLM_CONFIGS", [ - {"id": -2, "provider": "OPENAI", "model_name": "gpt-free", "api_key": "k1", "billing_tier": "free"}, - {"id": -1, "provider": "OPENAI", "model_name": "gpt-prem", "api_key": "k2", "billing_tier": "premium"}, + { + "id": -2, + "provider": "OPENAI", + "model_name": "gpt-free", + "api_key": "k1", + "billing_tier": "free", + }, + { + "id": -1, + "provider": "OPENAI", + "model_name": "gpt-prem", + "api_key": "k2", + "billing_tier": "premium", + }, ], ) diff --git a/surfsense_backend/tests/unit/test_stream_new_chat_contract.py b/surfsense_backend/tests/unit/test_stream_new_chat_contract.py index a1345c15c..5e6ad6abd 100644 --- a/surfsense_backend/tests/unit/test_stream_new_chat_contract.py +++ b/surfsense_backend/tests/unit/test_stream_new_chat_contract.py @@ -203,7 +203,10 @@ def test_stream_exception_classifies_turn_cancelling_when_cancel_requested(): def test_premium_classification_is_error_code_driven(): - classifier_path = Path(__file__).resolve().parents[3] / "surfsense_web/lib/chat/chat-error-classifier.ts" + classifier_path = ( + Path(__file__).resolve().parents[3] + / "surfsense_web/lib/chat/chat-error-classifier.ts" + ) source = classifier_path.read_text(encoding="utf-8") assert "PREMIUM_KEYWORDS" not in source @@ -229,7 +232,8 @@ def test_stream_terminal_error_handler_has_pre_accept_soft_rollback_hook(): def test_toast_only_pre_accept_policy_has_no_inline_failed_marker(): user_message_path = ( - Path(__file__).resolve().parents[3] / "surfsense_web/components/assistant-ui/user-message.tsx" + Path(__file__).resolve().parents[3] + / "surfsense_web/components/assistant-ui/user-message.tsx" ) source = user_message_path.read_text(encoding="utf-8") @@ -238,10 +242,14 @@ def test_toast_only_pre_accept_policy_has_no_inline_failed_marker(): def test_network_send_failures_use_unified_retry_toast_message(): - classifier_path = Path(__file__).resolve().parents[3] / "surfsense_web/lib/chat/chat-error-classifier.ts" + classifier_path = ( + Path(__file__).resolve().parents[3] + / "surfsense_web/lib/chat/chat-error-classifier.ts" + ) classifier_source = classifier_path.read_text(encoding="utf-8") request_errors_path = ( - Path(__file__).resolve().parents[3] / "surfsense_web/lib/chat/chat-request-errors.ts" + Path(__file__).resolve().parents[3] + / "surfsense_web/lib/chat/chat-request-errors.ts" ) request_errors_source = request_errors_path.read_text(encoding="utf-8") @@ -350,15 +358,17 @@ def test_turn_status_sse_contract_exists(): / "surfsense_backend/app/tasks/chat/stream_new_chat.py" ).read_text(encoding="utf-8") state_source = ( - Path(__file__).resolve().parents[3] / "surfsense_web/lib/chat/streaming-state.ts" + Path(__file__).resolve().parents[3] + / "surfsense_web/lib/chat/streaming-state.ts" ).read_text(encoding="utf-8") pipeline_source = ( - Path(__file__).resolve().parents[3] / "surfsense_web/lib/chat/stream-pipeline.ts" + Path(__file__).resolve().parents[3] + / "surfsense_web/lib/chat/stream-pipeline.ts" ).read_text(encoding="utf-8") assert '"turn-status"' in stream_source assert '"status": "busy"' in stream_source assert '"status": "idle"' in stream_source - assert "type: \"data-turn-status\"" in state_source - assert "case \"data-turn-status\":" in pipeline_source + assert 'type: "data-turn-status"' in state_source + assert 'case "data-turn-status":' in pipeline_source assert "end_turn(str(chat_id))" in stream_source diff --git a/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx index 1b25ca431..39201e5cc 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx @@ -19,7 +19,6 @@ import { currentThreadAtom, setTargetCommentIdAtom, } from "@/atoms/chat/current-thread.atom"; -import { setPremiumAlertForThreadAtom } from "@/atoms/chat/premium-alert.atom"; import { type MentionedDocumentInfo, mentionedDocumentIdsAtom, @@ -31,6 +30,7 @@ import { clearPlanOwnerRegistry, // extractWriteTodosFromContent, } from "@/atoms/chat/plan-state.atom"; +import { setPremiumAlertForThreadAtom } from "@/atoms/chat/premium-alert.atom"; import { closeReportPanelAtom } from "@/atoms/chat/report-panel.atom"; import { type AgentCreatedDocument, agentCreatedDocumentsAtom } from "@/atoms/documents/ui.atoms"; import { closeEditorPanelAtom } from "@/atoms/editor/editor-panel.atom"; @@ -60,20 +60,28 @@ import { useMessagesSync } from "@/hooks/use-messages-sync"; import { getAgentFilesystemSelection } from "@/lib/agent-filesystem"; import { documentsApiService } from "@/lib/apis/documents-api.service"; import { getBearerToken } from "@/lib/auth-utils"; -import { - classifyChatError, - type ChatFlow, -} from "@/lib/chat/chat-error-classifier"; -import { - tagPreAcceptSendFailure, - toHttpResponseError, -} from "@/lib/chat/chat-request-errors"; +import { type ChatFlow, classifyChatError } from "@/lib/chat/chat-error-classifier"; +import { tagPreAcceptSendFailure, toHttpResponseError } from "@/lib/chat/chat-request-errors"; import { convertToThreadMessage } from "@/lib/chat/message-utils"; import { isPodcastGenerating, looksLikePodcastRequest, setActivePodcastTaskId, } from "@/lib/chat/podcast-state"; +import { createStreamFlushHelpers } from "@/lib/chat/stream-flush"; +import { + consumeSseEvents, + hasPersistableContent, + processSharedStreamEvent, +} from "@/lib/chat/stream-pipeline"; +import { + applyInterruptRequestToContentParts, + applyTurnIdToAssistantMessageList, + markInterruptDecisionOnContentParts, + mergeChatTurnIdIntoMessage, + mergeEditedInterruptAction, + readStreamedChatTurnId, +} from "@/lib/chat/stream-side-effects"; import { buildContentForPersistence, buildContentForUI, @@ -82,20 +90,6 @@ import { type ThinkingStepData, type ToolUIGate, } from "@/lib/chat/streaming-state"; -import { createStreamFlushHelpers } from "@/lib/chat/stream-flush"; -import { - consumeSseEvents, - hasPersistableContent, - processSharedStreamEvent, -} from "@/lib/chat/stream-pipeline"; -import { - applyTurnIdToAssistantMessageList, - applyInterruptRequestToContentParts, - mergeChatTurnIdIntoMessage, - mergeEditedInterruptAction, - markInterruptDecisionOnContentParts, - readStreamedChatTurnId, -} from "@/lib/chat/stream-side-effects"; import { appendMessage, createThread, @@ -112,8 +106,8 @@ import { } from "@/lib/chat/user-turn-api-parts"; import { NotFoundError } from "@/lib/error"; import { - trackChatCreated, trackChatBlocked, + trackChatCreated, trackChatErrorDetailed, trackChatMessageSent, trackChatResponseReceived, @@ -193,7 +187,8 @@ function sleep(ms: number): Promise { function computeFallbackTurnCancellingRetryDelay(attempt: number): number { const safeAttempt = Math.max(1, attempt); - const raw = TURN_CANCELLING_INITIAL_DELAY_MS * TURN_CANCELLING_BACKOFF_FACTOR ** (safeAttempt - 1); + const raw = + TURN_CANCELLING_INITIAL_DELAY_MS * TURN_CANCELLING_BACKOFF_FACTOR ** (safeAttempt - 1); return Math.min(raw, TURN_CANCELLING_MAX_DELAY_MS); } @@ -278,11 +273,9 @@ export default function NewChatPage() { }) => { if (!threadId) return null; try { - const normalizedContent = Array.isArray(content) - ? ([...content] as unknown[]) - : [content]; - const hasMentionedDocumentsPart = normalizedContent.some((part) => - MentionedDocumentsPartSchema.safeParse(part).success + const normalizedContent = Array.isArray(content) ? ([...content] as unknown[]) : [content]; + const hasMentionedDocumentsPart = normalizedContent.some( + (part) => MentionedDocumentsPartSchema.safeParse(part).success ); if (mentionedDocs && mentionedDocs.length > 0 && !hasMentionedDocumentsPart) { normalizedContent.push({ @@ -300,10 +293,7 @@ export default function NewChatPage() { setMessages((prev) => prev.map((m) => m.id === userMsgId - ? mergeChatTurnIdIntoMessage( - { ...m, id: newUserMsgId }, - savedUserMessage.turn_id - ) + ? mergeChatTurnIdIntoMessage({ ...m, id: newUserMsgId }, savedUserMessage.turn_id) : m ) ); @@ -356,10 +346,7 @@ export default function NewChatPage() { setMessages((prev) => prev.map((m) => m.id === assistantMsgId - ? mergeChatTurnIdIntoMessage( - { ...m, id: newMsgId }, - savedMessage.turn_id - ) + ? mergeChatTurnIdIntoMessage({ ...m, id: newMsgId }, savedMessage.turn_id) : m ) ); @@ -564,12 +551,7 @@ export default function NewChatPage() { toast.error(normalized.userMessage); }, - [ - currentUser?.id, - persistAssistantErrorMessage, - searchSpaceId, - setPremiumAlertForThread, - ] + [currentUser?.id, persistAssistantErrorMessage, searchSpaceId, setPremiumAlertForThread] ); const handleStreamTerminalError = useCallback( @@ -613,35 +595,31 @@ export default function NewChatPage() { [handleChatFailure] ); - const fetchWithTurnCancellingRetry = useCallback( - async (runFetch: () => Promise) => { - const maxAttempts = 4; - for (let attempt = 1; attempt <= maxAttempts; attempt += 1) { - const response = await runFetch(); - if (response.ok) { - return response; - } - const error = await toHttpResponseError(response); - const withMeta = error as Error & { errorCode?: string; retryAfterMs?: number }; - const isTurnCancelling = withMeta.errorCode === "TURN_CANCELLING"; - const isRecentThreadBusyAfterCancel = - withMeta.errorCode === "THREAD_BUSY" && - Date.now() - recentCancelRequestedAtRef.current <= RECENT_CANCEL_WINDOW_MS; - if ((isTurnCancelling || isRecentThreadBusyAfterCancel) && attempt < maxAttempts) { - const waitMs = - withMeta.retryAfterMs ?? computeFallbackTurnCancellingRetryDelay(attempt); - await sleep(waitMs); - continue; - } - throw error; + const fetchWithTurnCancellingRetry = useCallback(async (runFetch: () => Promise) => { + const maxAttempts = 4; + for (let attempt = 1; attempt <= maxAttempts; attempt += 1) { + const response = await runFetch(); + if (response.ok) { + return response; } + const error = await toHttpResponseError(response); + const withMeta = error as Error & { errorCode?: string; retryAfterMs?: number }; + const isTurnCancelling = withMeta.errorCode === "TURN_CANCELLING"; + const isRecentThreadBusyAfterCancel = + withMeta.errorCode === "THREAD_BUSY" && + Date.now() - recentCancelRequestedAtRef.current <= RECENT_CANCEL_WINDOW_MS; + if ((isTurnCancelling || isRecentThreadBusyAfterCancel) && attempt < maxAttempts) { + const waitMs = withMeta.retryAfterMs ?? computeFallbackTurnCancellingRetryDelay(attempt); + await sleep(waitMs); + continue; + } + throw error; + } - throw Object.assign(new Error("Turn cancellation retry limit exceeded"), { - errorCode: "TURN_CANCELLING", - }); - }, - [] - ); + throw Object.assign(new Error("Turn cancellation retry limit exceeded"), { + errorCode: "TURN_CANCELLING", + }); + }, []); // Initialize thread and load messages // For new chats (no urlChatId), we use lazy creation - thread is created on first message diff --git a/surfsense_web/app/desktop/permissions/page.tsx b/surfsense_web/app/desktop/permissions/page.tsx index e30a76f83..ca9228272 100644 --- a/surfsense_web/app/desktop/permissions/page.tsx +++ b/surfsense_web/app/desktop/permissions/page.tsx @@ -132,8 +132,8 @@ export default function DesktopPermissionsPage() {

System Permissions

- SurfSense needs two macOS permissions for Screenshot Assist and for desktop features that - require focusing the app or the active application. + SurfSense needs two macOS permissions for Screenshot Assist and for desktop features + that require focusing the app or the active application.

diff --git a/surfsense_web/components/agent-action-log/action-log-sheet.tsx b/surfsense_web/components/agent-action-log/action-log-sheet.tsx index 32c25771a..7d27b4019 100644 --- a/surfsense_web/components/agent-action-log/action-log-sheet.tsx +++ b/surfsense_web/components/agent-action-log/action-log-sheet.tsx @@ -17,10 +17,7 @@ import { SheetTitle, } from "@/components/ui/sheet"; import { Skeleton } from "@/components/ui/skeleton"; -import { - agentActionsQueryKey, - useAgentActionsQuery, -} from "@/hooks/use-agent-actions-query"; +import { agentActionsQueryKey, useAgentActionsQuery } from "@/hooks/use-agent-actions-query"; import { ActionLogItem } from "./action-log-item"; function EmptyState() { diff --git a/surfsense_web/components/assistant-ui/inline-citation.tsx b/surfsense_web/components/assistant-ui/inline-citation.tsx index e299f2373..32a29cfc9 100644 --- a/surfsense_web/components/assistant-ui/inline-citation.tsx +++ b/surfsense_web/components/assistant-ui/inline-citation.tsx @@ -182,11 +182,7 @@ const SurfsenseDocCitation: FC<{ chunkId: number }> = ({ chunkId }) => {

)} {!isLoading && !error && citedChunk?.content && ( - + )} {!isLoading && !error && !citedChunk?.content && (

No content available.

diff --git a/surfsense_web/components/assistant-ui/inline-mention-editor.tsx b/surfsense_web/components/assistant-ui/inline-mention-editor.tsx index d92348080..c585dc80f 100644 --- a/surfsense_web/components/assistant-ui/inline-mention-editor.tsx +++ b/surfsense_web/components/assistant-ui/inline-mention-editor.tsx @@ -1,8 +1,14 @@ "use client"; -import { type FC, forwardRef, useCallback, useImperativeHandle, useMemo, useRef } from "react"; -import { Plate, PlateContent, ParagraphPlugin, createPlatePlugin, usePlateEditor } from "platejs/react"; import type { PlateElementProps } from "platejs/react"; +import { + createPlatePlugin, + ParagraphPlugin, + Plate, + PlateContent, + usePlateEditor, +} from "platejs/react"; +import { type FC, forwardRef, useCallback, useImperativeHandle, useMemo, useRef } from "react"; import { getConnectorIcon } from "@/contracts/enums/connectorIcons"; import type { Document } from "@/contracts/types/document.types"; import { getMentionDocKey } from "@/lib/chat/mention-doc-key"; @@ -72,7 +78,11 @@ const COMPOSER_TEXT_METRICS_CLASSNAME = "text-sm leading-6"; const EMPTY_VALUE: ComposerValue = [{ type: "p", children: [{ text: "" }] }]; -const MentionElement: FC> = ({ attributes, children, element }) => { +const MentionElement: FC> = ({ + attributes, + children, + element, +}) => { const statusClass = element.statusKind === "failed" ? "text-destructive" @@ -255,7 +265,10 @@ export const InlineMentionEditor = forwardRef (editor.children as ComposerValue) ?? EMPTY_VALUE, [editor]); + const getCurrentValue = useCallback( + () => (editor.children as ComposerValue) ?? EMPTY_VALUE, + [editor] + ); const emitState = useCallback( (nextValue: ComposerValue) => { @@ -379,7 +392,8 @@ export const InlineMentionEditor = forwardRef { const children = block.children.filter((node) => { if (!isMentionNode(node)) return true; - const match = node.id === docId && (node.document_type ?? "UNKNOWN") === (docType ?? "UNKNOWN"); + const match = + node.id === docId && (node.document_type ?? "UNKNOWN") === (docType ?? "UNKNOWN"); if (match) changed = true; return !match; }); @@ -450,7 +464,15 @@ export const InlineMentionEditor = forwardRef { const urlMapRef = useRef(EMPTY_URL_MAP); - const preprocess = useCallback( - (content: string) => preprocessMarkdown(content, urlMapRef), - [] - ); + const preprocess = useCallback((content: string) => preprocessMarkdown(content, urlMapRef), []); return ( {processChildrenWithCitations(children, urlMap)} diff --git a/surfsense_web/components/assistant-ui/nested-scroll.tsx b/surfsense_web/components/assistant-ui/nested-scroll.tsx index 5a4f8d36e..37c4790df 100644 --- a/surfsense_web/components/assistant-ui/nested-scroll.tsx +++ b/surfsense_web/components/assistant-ui/nested-scroll.tsx @@ -1,6 +1,6 @@ "use client"; -import { forwardRef, type ComponentPropsWithoutRef, type WheelEvent } from "react"; +import { type ComponentPropsWithoutRef, forwardRef, type WheelEvent } from "react"; export type NestedScrollProps = ComponentPropsWithoutRef<"div">; diff --git a/surfsense_web/components/assistant-ui/thread.tsx b/surfsense_web/components/assistant-ui/thread.tsx index 6c02a1efa..b4a3b58c6 100644 --- a/surfsense_web/components/assistant-ui/thread.tsx +++ b/surfsense_web/components/assistant-ui/thread.tsx @@ -92,8 +92,8 @@ import { useBatchCommentsPreload } from "@/hooks/use-comments"; import { useCommentsSync } from "@/hooks/use-comments-sync"; import { useMediaQuery } from "@/hooks/use-media-query"; import { useElectronAPI } from "@/hooks/use-platform"; -import { getMentionDocKey } from "@/lib/chat/mention-doc-key"; import { captureDisplayToPngDataUrl } from "@/lib/chat/display-media-capture"; +import { getMentionDocKey } from "@/lib/chat/mention-doc-key"; import { SLIDEOUT_PANEL_OPENED_EVENT } from "@/lib/layout-events"; import { cn } from "@/lib/utils"; diff --git a/surfsense_web/components/assistant-ui/tool-fallback.tsx b/surfsense_web/components/assistant-ui/tool-fallback.tsx index cf42cf398..06082c9c7 100644 --- a/surfsense_web/components/assistant-ui/tool-fallback.tsx +++ b/surfsense_web/components/assistant-ui/tool-fallback.tsx @@ -1,19 +1,16 @@ -import { - type ToolCallMessagePartComponent, - useAuiState, -} from "@assistant-ui/react"; +import { type ToolCallMessagePartComponent, useAuiState } from "@assistant-ui/react"; import { useQueryClient } from "@tanstack/react-query"; import { useAtomValue } from "jotai"; import { CheckIcon, ChevronDownIcon, RotateCcw, XCircleIcon } from "lucide-react"; import { useEffect, useMemo, useState } from "react"; import { toast } from "sonner"; import { chatSessionStateAtom } from "@/atoms/chat/chat-session-state.atom"; +import { NestedScroll } from "@/components/assistant-ui/nested-scroll"; import { DoomLoopApprovalToolUI, isDoomLoopInterrupt, } from "@/components/tool-ui/doom-loop-approval"; import { GenericHitlApprovalToolUI } from "@/components/tool-ui/generic-hitl-approval"; -import { NestedScroll } from "@/components/assistant-ui/nested-scroll"; import { AlertDialog, AlertDialogAction, @@ -32,10 +29,7 @@ import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/component import { Separator } from "@/components/ui/separator"; import { Spinner } from "@/components/ui/spinner"; import { getToolDisplayName } from "@/contracts/enums/toolIcons"; -import { - markActionRevertedInCache, - useAgentActionsQuery, -} from "@/hooks/use-agent-actions-query"; +import { markActionRevertedInCache, useAgentActionsQuery } from "@/hooks/use-agent-actions-query"; import { agentActionsApiService } from "@/lib/apis/agent-actions-api.service"; import { AppError } from "@/lib/error"; import { isInterruptResult } from "@/lib/hitl"; @@ -124,8 +118,7 @@ function ToolCardRevertButton({ // Tier 1 + 2: O(1) Map-backed direct id match. Covers // ~all parity_v2 streams and any legacy stream that backfilled // ``langchainToolCallId`` via ``tool-output-available``. - const direct = - findByToolCallId(toolCallId) ?? findByToolCallId(langchainToolCallId); + const direct = findByToolCallId(toolCallId) ?? findByToolCallId(langchainToolCallId); if (direct) return direct; // Tier 3: position-within-turn fallback. Only kicks in when the // card has a synthetic ``call_`` id AND no @@ -160,12 +153,7 @@ function ToolCardRevertButton({ setIsReverting(true); try { const response = await agentActionsApiService.revert(threadId, action.id); - markActionRevertedInCache( - queryClient, - threadId, - action.id, - response.new_action_id ?? null - ); + markActionRevertedInCache(queryClient, threadId, action.id, response.new_action_id ?? null); toast.success(response.message || "Action reverted."); } catch (err) { // 503 means revert is gated off on this deployment — hide the diff --git a/surfsense_web/components/citations/citation-renderer.tsx b/surfsense_web/components/citations/citation-renderer.tsx index bf877f03f..f2de4b27d 100644 --- a/surfsense_web/components/citations/citation-renderer.tsx +++ b/surfsense_web/components/citations/citation-renderer.tsx @@ -64,9 +64,7 @@ export function processChildrenWithCitations( return ( {segments.map((segment) => - typeof segment === "string" - ? segment - : renderCitationToken(segment, ordinal++) + typeof segment === "string" ? segment : renderCitationToken(segment, ordinal++) )} ); diff --git a/surfsense_web/components/editor/plugins/citation-kit.tsx b/surfsense_web/components/editor/plugins/citation-kit.tsx index c90cb5e28..1908de209 100644 --- a/surfsense_web/components/editor/plugins/citation-kit.tsx +++ b/surfsense_web/components/editor/plugins/citation-kit.tsx @@ -1,8 +1,8 @@ "use client"; -import { type FC } from "react"; -import { KEYS, type Descendant } from "platejs"; +import { type Descendant, KEYS } from "platejs"; import { createPlatePlugin, type PlateElementProps } from "platejs/react"; +import type { FC } from "react"; import { InlineCitation, UrlCitation } from "@/components/assistant-ui/inline-citation"; import { CITATION_REGEX, @@ -97,11 +97,7 @@ function asElement(node: Descendant): SlateElement { * swallows the citation click. Mirrors the `` skip in * `MarkdownViewer`. */ -const SKIP_SUBTREE_TYPES = new Set([ - KEYS.codeBlock, - "code_line", - KEYS.link, -]); +const SKIP_SUBTREE_TYPES = new Set([KEYS.codeBlock, "code_line", KEYS.link]); /** * Build the marks portion of a Slate text node so we can preserve formatting diff --git a/surfsense_web/components/layout/providers/LayoutDataProvider.tsx b/surfsense_web/components/layout/providers/LayoutDataProvider.tsx index 3efdab03b..afd888f48 100644 --- a/surfsense_web/components/layout/providers/LayoutDataProvider.tsx +++ b/surfsense_web/components/layout/providers/LayoutDataProvider.tsx @@ -26,9 +26,9 @@ import { type Tab, } from "@/atoms/tabs/tabs.atom"; import { currentUserAtom } from "@/atoms/user/user-query.atoms"; +import { ActionLogSheet } from "@/components/agent-action-log/action-log-sheet"; import { SearchSpaceSettingsDialog } from "@/components/settings/search-space-settings-dialog"; import { TeamDialog } from "@/components/settings/team-dialog"; -import { ActionLogSheet } from "@/components/agent-action-log/action-log-sheet"; import { UserSettingsDialog } from "@/components/settings/user-settings-dialog"; import { AlertDialog, diff --git a/surfsense_web/components/layout/ui/sidebar/DocumentsSidebar.tsx b/surfsense_web/components/layout/ui/sidebar/DocumentsSidebar.tsx index d20aea2cd..bf4de6454 100644 --- a/surfsense_web/components/layout/ui/sidebar/DocumentsSidebar.tsx +++ b/surfsense_web/components/layout/ui/sidebar/DocumentsSidebar.tsx @@ -23,9 +23,7 @@ import { useTranslations } from "next-intl"; import type React from "react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { toast } from "sonner"; -import { - mentionedDocumentsAtom, -} from "@/atoms/chat/mentioned-documents.atom"; +import { mentionedDocumentsAtom } from "@/atoms/chat/mentioned-documents.atom"; import { connectorDialogOpenAtom } from "@/atoms/connector-dialog/connector-dialog.atoms"; import { connectorsAtom } from "@/atoms/connectors/connector-query.atoms"; import { deleteDocumentMutationAtom } from "@/atoms/documents/document-mutation.atoms"; @@ -74,12 +72,12 @@ import type { DocumentTypeEnum } from "@/contracts/types/document.types"; import { useDebouncedValue } from "@/hooks/use-debounced-value"; import { useMediaQuery } from "@/hooks/use-media-query"; import { useElectronAPI, usePlatform } from "@/hooks/use-platform"; -import { getMentionDocKey } from "@/lib/chat/mention-doc-key"; import { anonymousChatApiService } from "@/lib/apis/anonymous-chat-api.service"; import { documentsApiService } from "@/lib/apis/documents-api.service"; import { foldersApiService } from "@/lib/apis/folders-api.service"; import { searchSpacesApiService } from "@/lib/apis/search-spaces-api.service"; import { authenticatedFetch } from "@/lib/auth-utils"; +import { getMentionDocKey } from "@/lib/chat/mention-doc-key"; import { uploadFolderScan } from "@/lib/folder-sync-upload"; import { getSupportedExtensionsSet } from "@/lib/supported-extensions"; import { queries } from "@/zero/queries/index"; diff --git a/surfsense_web/components/markdown-viewer.tsx b/surfsense_web/components/markdown-viewer.tsx index b2420711a..6caf01917 100644 --- a/surfsense_web/components/markdown-viewer.tsx +++ b/surfsense_web/components/markdown-viewer.tsx @@ -5,10 +5,7 @@ import "katex/dist/katex.min.css"; import Image from "next/image"; import { useMemo } from "react"; import { processChildrenWithCitations } from "@/components/citations/citation-renderer"; -import { - type CitationUrlMap, - preprocessCitationMarkdown, -} from "@/lib/citations/citation-parser"; +import { type CitationUrlMap, preprocessCitationMarkdown } from "@/lib/citations/citation-parser"; import { cn } from "@/lib/utils"; const code = createCodePlugin({ diff --git a/surfsense_web/hooks/use-agent-actions-query.ts b/surfsense_web/hooks/use-agent-actions-query.ts index 9a722fb2e..114c79567 100644 --- a/surfsense_web/hooks/use-agent-actions-query.ts +++ b/surfsense_web/hooks/use-agent-actions-query.ts @@ -88,71 +88,68 @@ export function applyActionLogSse( searchSpaceId, event, }); - queryClient.setQueryData( - agentActionsQueryKey(threadId), - (prev) => { - const placeholder: AgentAction = { - id: event.id, - thread_id: threadId, - user_id: null, - search_space_id: searchSpaceId, - tool_name: event.tool_name, - args: null, - result_id: null, - reversible: event.reversible, - reverse_descriptor: event.reverse_descriptor_present ? {} : null, - error: event.error ? {} : null, - reverse_of: null, - reverted_by_action_id: null, - is_revert_action: false, - tool_call_id: event.lc_tool_call_id, - chat_turn_id: event.chat_turn_id, - created_at: event.created_at ?? new Date().toISOString(), - }; - if (!prev) { - return { - items: [placeholder], - total: 1, - page: 0, - page_size: ACTION_LOG_PAGE_SIZE, - has_more: false, - }; - } - const existingIdx = prev.items.findIndex((a) => a.id === event.id); - if (existingIdx >= 0) { - const merged = [...prev.items]; - const existing = merged[existingIdx]; - if (existing) { - merged[existingIdx] = { - ...existing, - reversible: event.reversible, - tool_call_id: event.lc_tool_call_id ?? existing.tool_call_id, - chat_turn_id: event.chat_turn_id ?? existing.chat_turn_id, - }; - } - dbg("applyActionLogSse: merged into existing entry", { - id: event.id, - tool_call_id: merged[existingIdx]?.tool_call_id, - reversible: merged[existingIdx]?.reversible, - }); - return { ...prev, items: merged }; - } - dbg("applyActionLogSse: appended new placeholder", { - id: event.id, - tool_call_id: placeholder.tool_call_id, - tool_name: placeholder.tool_name, - reversible: placeholder.reversible, - cacheSizeAfter: prev.items.length + 1, - }); - // REST returns newest-first — keep that ordering when - // the server eventually refetches by prepending. + queryClient.setQueryData(agentActionsQueryKey(threadId), (prev) => { + const placeholder: AgentAction = { + id: event.id, + thread_id: threadId, + user_id: null, + search_space_id: searchSpaceId, + tool_name: event.tool_name, + args: null, + result_id: null, + reversible: event.reversible, + reverse_descriptor: event.reverse_descriptor_present ? {} : null, + error: event.error ? {} : null, + reverse_of: null, + reverted_by_action_id: null, + is_revert_action: false, + tool_call_id: event.lc_tool_call_id, + chat_turn_id: event.chat_turn_id, + created_at: event.created_at ?? new Date().toISOString(), + }; + if (!prev) { return { - ...prev, - items: [placeholder, ...prev.items], - total: prev.total + 1, + items: [placeholder], + total: 1, + page: 0, + page_size: ACTION_LOG_PAGE_SIZE, + has_more: false, }; } - ); + const existingIdx = prev.items.findIndex((a) => a.id === event.id); + if (existingIdx >= 0) { + const merged = [...prev.items]; + const existing = merged[existingIdx]; + if (existing) { + merged[existingIdx] = { + ...existing, + reversible: event.reversible, + tool_call_id: event.lc_tool_call_id ?? existing.tool_call_id, + chat_turn_id: event.chat_turn_id ?? existing.chat_turn_id, + }; + } + dbg("applyActionLogSse: merged into existing entry", { + id: event.id, + tool_call_id: merged[existingIdx]?.tool_call_id, + reversible: merged[existingIdx]?.reversible, + }); + return { ...prev, items: merged }; + } + dbg("applyActionLogSse: appended new placeholder", { + id: event.id, + tool_call_id: placeholder.tool_call_id, + tool_name: placeholder.tool_name, + reversible: placeholder.reversible, + cacheSizeAfter: prev.items.length + 1, + }); + // REST returns newest-first — keep that ordering when + // the server eventually refetches by prepending. + return { + ...prev, + items: [placeholder, ...prev.items], + total: prev.total + 1, + }; + }); } /** @@ -170,33 +167,30 @@ export function applyActionLogUpdatedSse( id, reversible, }); - queryClient.setQueryData( - agentActionsQueryKey(threadId), - (prev) => { - if (!prev) { - dbg("applyActionLogUpdatedSse: NO prev cache for thread; flip dropped", { - threadId, - id, - }); - return prev; - } - let mutated = false; - const items = prev.items.map((a) => { - if (a.id !== id) return a; - mutated = true; - return { ...a, reversible }; + queryClient.setQueryData(agentActionsQueryKey(threadId), (prev) => { + if (!prev) { + dbg("applyActionLogUpdatedSse: NO prev cache for thread; flip dropped", { + threadId, + id, }); - if (!mutated) { - dbg("applyActionLogUpdatedSse: id not in cache; flip dropped", { - threadId, - id, - cacheSize: prev.items.length, - cacheIds: prev.items.map((a) => a.id), - }); - } - return mutated ? { ...prev, items } : prev; + return prev; } - ); + let mutated = false; + const items = prev.items.map((a) => { + if (a.id !== id) return a; + mutated = true; + return { ...a, reversible }; + }); + if (!mutated) { + dbg("applyActionLogUpdatedSse: id not in cache; flip dropped", { + threadId, + id, + cacheSize: prev.items.length, + cacheIds: prev.items.map((a) => a.id), + }); + } + return mutated ? { ...prev, items } : prev; + }); } /** @@ -214,24 +208,21 @@ export function markActionRevertedInCache( id: number, newActionId: number | null ): void { - queryClient.setQueryData( - agentActionsQueryKey(threadId), - (prev) => { - if (!prev) return prev; - let mutated = false; - const items = prev.items.map((a) => { - if (a.id !== id) return a; - mutated = true; - // ``-1`` is a sentinel meaning "we know it was reverted - // but the server didn't tell us the new row's id". - return { - ...a, - reverted_by_action_id: newActionId ?? -1, - }; - }); - return mutated ? { ...prev, items } : prev; - } - ); + queryClient.setQueryData(agentActionsQueryKey(threadId), (prev) => { + if (!prev) return prev; + let mutated = false; + const items = prev.items.map((a) => { + if (a.id !== id) return a; + mutated = true; + // ``-1`` is a sentinel meaning "we know it was reverted + // but the server didn't tell us the new row's id". + return { + ...a, + reverted_by_action_id: newActionId ?? -1, + }; + }); + return mutated ? { ...prev, items } : prev; + }); } /** @@ -245,21 +236,18 @@ export function applyRevertTurnResultsToCache( entries: Array<{ id: number; newActionId: number | null }> ): void { if (entries.length === 0) return; - queryClient.setQueryData( - agentActionsQueryKey(threadId), - (prev) => { - if (!prev) return prev; - const lookup = new Map(entries.map((e) => [e.id, e.newActionId])); - let mutated = false; - const items = prev.items.map((a) => { - if (!lookup.has(a.id)) return a; - mutated = true; - const newActionId = lookup.get(a.id) ?? null; - return { ...a, reverted_by_action_id: newActionId ?? -1 }; - }); - return mutated ? { ...prev, items } : prev; - } - ); + queryClient.setQueryData(agentActionsQueryKey(threadId), (prev) => { + if (!prev) return prev; + const lookup = new Map(entries.map((e) => [e.id, e.newActionId])); + let mutated = false; + const items = prev.items.map((a) => { + if (!lookup.has(a.id)) return a; + mutated = true; + const newActionId = lookup.get(a.id) ?? null; + return { ...a, reverted_by_action_id: newActionId ?? -1 }; + }); + return mutated ? { ...prev, items } : prev; + }); } /** @@ -271,10 +259,7 @@ export function applyRevertTurnResultsToCache( * knob — pass ``false`` to keep the query dormant when the consumer * doesn't yet have a thread id. */ -export function useAgentActionsQuery( - threadId: number | null, - options: { enabled?: boolean } = {} -) { +export function useAgentActionsQuery(threadId: number | null, options: { enabled?: boolean } = {}) { const enabled = (options.enabled ?? true) && threadId !== null; const query = useQuery({ queryKey: agentActionsQueryKey(threadId), @@ -336,10 +321,7 @@ export function useAgentActionsQuery( else m.set(key, [a]); } for (const bucket of m.values()) { - bucket.sort( - (a, b) => - new Date(a.created_at).getTime() - new Date(b.created_at).getTime() - ); + bucket.sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime()); } return m; }, [items]); @@ -396,10 +378,7 @@ export function useAgentActionsQuery( ); const findByChatTurnAndTool = useCallback( - ( - chatTurnId: string | null | undefined, - toolName: string | null | undefined - ): AgentAction[] => { + (chatTurnId: string | null | undefined, toolName: string | null | undefined): AgentAction[] => { if (!chatTurnId || !toolName) return []; return byTurnAndTool.get(`${chatTurnId}::${toolName}`) ?? []; }, diff --git a/surfsense_web/lib/chat/chat-error-classifier.ts b/surfsense_web/lib/chat/chat-error-classifier.ts index 7dfbfc1a1..95d9848f2 100644 --- a/surfsense_web/lib/chat/chat-error-classifier.ts +++ b/surfsense_web/lib/chat/chat-error-classifier.ts @@ -53,7 +53,10 @@ function getErrorMessage(error: unknown): string { } } -function getErrorCode(error: unknown, parsedJson: Record | null): string | undefined { +function getErrorCode( + error: unknown, + parsedJson: Record | null +): string | undefined { if (error instanceof Error) { const withCode = error as Error & { errorCode?: string; code?: string }; if (withCode.errorCode) return withCode.errorCode; @@ -138,8 +141,7 @@ export function classifyChatError(input: RawChatErrorInput): NormalizedChatError severity: "info", telemetryEvent: "chat_blocked", isExpected: true, - userMessage: - "Buy more tokens to continue with this model, or switch to a free model.", + userMessage: "Buy more tokens to continue with this model, or switch to a free model.", assistantMessage: PREMIUM_QUOTA_ASSISTANT_MESSAGE, rawMessage, errorCode: errorCode ?? "PREMIUM_QUOTA_EXHAUSTED", @@ -147,9 +149,7 @@ export function classifyChatError(input: RawChatErrorInput): NormalizedChatError }; } - if ( - errorCode === "TURN_CANCELLING" - ) { + if (errorCode === "TURN_CANCELLING") { return { kind: "thread_busy", channel: "toast", @@ -163,16 +163,15 @@ export function classifyChatError(input: RawChatErrorInput): NormalizedChatError }; } - if ( - errorCode === "THREAD_BUSY" - ) { + if (errorCode === "THREAD_BUSY") { return { kind: "thread_busy", channel: "toast", severity: "warn", telemetryEvent: "chat_blocked", isExpected: true, - userMessage: "Another response is still finishing for this thread. Please try again in a moment.", + userMessage: + "Another response is still finishing for this thread. Please try again in a moment.", rawMessage, errorCode: errorCode ?? "THREAD_BUSY", details: { flow: input.flow }, @@ -193,10 +192,7 @@ export function classifyChatError(input: RawChatErrorInput): NormalizedChatError }; } - if ( - errorCode === "AUTH_EXPIRED" || - errorCode === "UNAUTHORIZED" - ) { + if (errorCode === "AUTH_EXPIRED" || errorCode === "UNAUTHORIZED") { return { kind: "auth_expired", channel: "toast", @@ -210,10 +206,7 @@ export function classifyChatError(input: RawChatErrorInput): NormalizedChatError }; } - if ( - errorCode === "RATE_LIMITED" || - providerTypeNormalized === "rate_limit_error" - ) { + if (errorCode === "RATE_LIMITED" || providerTypeNormalized === "rate_limit_error") { return { kind: "rate_limited", channel: "toast", @@ -242,9 +235,7 @@ export function classifyChatError(input: RawChatErrorInput): NormalizedChatError }; } - if ( - errorCode === "STREAM_PARSE_ERROR" - ) { + if (errorCode === "STREAM_PARSE_ERROR") { return { kind: "stream_parse_error", channel: "toast", @@ -258,9 +249,7 @@ export function classifyChatError(input: RawChatErrorInput): NormalizedChatError }; } - if ( - errorCode === "TOOL_EXECUTION_ERROR" - ) { + if (errorCode === "TOOL_EXECUTION_ERROR") { return { kind: "tool_execution_error", channel: "toast", @@ -274,9 +263,7 @@ export function classifyChatError(input: RawChatErrorInput): NormalizedChatError }; } - if ( - errorCode === "PERSIST_MESSAGE_FAILED" - ) { + if (errorCode === "PERSIST_MESSAGE_FAILED") { return { kind: "persist_message_failed", channel: "toast", @@ -290,9 +277,7 @@ export function classifyChatError(input: RawChatErrorInput): NormalizedChatError }; } - if ( - errorCode === "SERVER_ERROR" - ) { + if (errorCode === "SERVER_ERROR") { return { kind: "server_error", channel: "toast", diff --git a/surfsense_web/lib/chat/chat-request-errors.ts b/surfsense_web/lib/chat/chat-request-errors.ts index 708831354..e0dfb3cc4 100644 --- a/surfsense_web/lib/chat/chat-request-errors.ts +++ b/surfsense_web/lib/chat/chat-request-errors.ts @@ -74,13 +74,9 @@ export async function toHttpResponseError( : Number.isFinite(retryAfterSeconds) ? Math.max(0, Math.round(retryAfterSeconds * 1000)) : undefined; - const retryAfterMs = - detailRetryAfterMs ?? topRetryAfterMs ?? retryAfterMsFromHeader ?? undefined; + const retryAfterMs = detailRetryAfterMs ?? topRetryAfterMs ?? retryAfterMsFromHeader ?? undefined; const message = - detailNestedMessage ?? - detailMessage ?? - topLevelMessage ?? - `Backend error: ${response.status}`; + detailNestedMessage ?? detailMessage ?? topLevelMessage ?? `Backend error: ${response.status}`; return Object.assign(new Error(message), { errorCode, retryAfterMs }); } diff --git a/surfsense_web/lib/chat/stream-pipeline.ts b/surfsense_web/lib/chat/stream-pipeline.ts index c9118f949..c76781083 100644 --- a/surfsense_web/lib/chat/stream-pipeline.ts +++ b/surfsense_web/lib/chat/stream-pipeline.ts @@ -72,8 +72,12 @@ function toStreamTerminalError( }); } -export function processSharedStreamEvent(parsed: SSEEvent, context: SharedStreamEventContext): boolean { - const { contentPartsState, toolsWithUI, currentThinkingSteps, scheduleFlush, forceFlush } = context; +export function processSharedStreamEvent( + parsed: SSEEvent, + context: SharedStreamEventContext +): boolean { + const { contentPartsState, toolsWithUI, currentThinkingSteps, scheduleFlush, forceFlush } = + context; const { contentParts, toolCallIndices } = contentPartsState; switch (parsed.type) { diff --git a/surfsense_web/lib/chat/stream-side-effects.ts b/surfsense_web/lib/chat/stream-side-effects.ts index 9cb349458..5483ff14b 100644 --- a/surfsense_web/lib/chat/stream-side-effects.ts +++ b/surfsense_web/lib/chat/stream-side-effects.ts @@ -16,9 +16,7 @@ export type EditedInterruptAction = { args: Record; }; -function readInterruptActions( - interruptData: Record -): InterruptActionRequest[] { +function readInterruptActions(interruptData: Record): InterruptActionRequest[] { return (interruptData.action_requests ?? []) as InterruptActionRequest[]; } @@ -121,7 +119,5 @@ export function applyTurnIdToAssistantMessageList( assistantMsgId: string, turnId: string ): ThreadMessageLike[] { - return messages.map((m) => - m.id === assistantMsgId ? mergeChatTurnIdIntoMessage(m, turnId) : m - ); + return messages.map((m) => (m.id === assistantMsgId ? mergeChatTurnIdIntoMessage(m, turnId) : m)); } diff --git a/surfsense_web/lib/citations/citation-parser.ts b/surfsense_web/lib/citations/citation-parser.ts index 6333b0f97..533c644c2 100644 --- a/surfsense_web/lib/citations/citation-parser.ts +++ b/surfsense_web/lib/citations/citation-parser.ts @@ -40,8 +40,7 @@ export interface PreprocessedCitations { } /** Pattern matching only URL-form citations (used during preprocessing). */ -const URL_CITATION_REGEX = - /[[【]\u200B?citation:\s*(https?:\/\/[^\]】\u200B]+)\s*\u200B?[\]】]/g; +const URL_CITATION_REGEX = /[[【]\u200B?citation:\s*(https?:\/\/[^\]】\u200B]+)\s*\u200B?[\]】]/g; /** * Replace `[citation:URL]` tokens with `[citation:urlciteN]` placeholders so @@ -82,10 +81,7 @@ export function preprocessCitationMarkdown(content: string): PreprocessedCitatio * tokens to JSX. Negative chunk IDs are forwarded as-is so the consumer * can decide how to render anonymous documents. */ -export function parseTextWithCitations( - text: string, - urlMap: CitationUrlMap -): ParsedSegment[] { +export function parseTextWithCitations(text: string, urlMap: CitationUrlMap): ParsedSegment[] { const segments: ParsedSegment[] = []; let lastIndex = 0; let match: RegExpExecArray | null; diff --git a/surfsense_web/lib/posthog/events.ts b/surfsense_web/lib/posthog/events.ts index 30e58215a..f9eb6b312 100644 --- a/surfsense_web/lib/posthog/events.ts +++ b/surfsense_web/lib/posthog/events.ts @@ -1,6 +1,6 @@ import posthog from "posthog-js"; import { getConnectorTelemetryMeta } from "@/components/assistant-ui/connector-popup/constants/connector-constants"; -import type { ChatErrorKind, ChatFlow, ChatErrorSeverity } from "@/lib/chat/chat-error-classifier"; +import type { ChatErrorKind, ChatErrorSeverity, ChatFlow } from "@/lib/chat/chat-error-classifier"; /** * PostHog Analytics Event Definitions