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