chore: linting
Some checks are pending
Build and Push Docker Images / tag_release (push) Waiting to run
Build and Push Docker Images / build (./surfsense_backend, ./surfsense_backend/Dockerfile, backend, surfsense-backend, ubuntu-24.04-arm, linux/arm64, arm64) (push) Blocked by required conditions
Build and Push Docker Images / build (./surfsense_backend, ./surfsense_backend/Dockerfile, backend, surfsense-backend, ubuntu-latest, linux/amd64, amd64) (push) Blocked by required conditions
Build and Push Docker Images / build (./surfsense_web, ./surfsense_web/Dockerfile, web, surfsense-web, ubuntu-24.04-arm, linux/arm64, arm64) (push) Blocked by required conditions
Build and Push Docker Images / build (./surfsense_web, ./surfsense_web/Dockerfile, web, surfsense-web, ubuntu-latest, linux/amd64, amd64) (push) Blocked by required conditions
Build and Push Docker Images / create_manifest (backend, surfsense-backend) (push) Blocked by required conditions
Build and Push Docker Images / create_manifest (web, surfsense-web) (push) Blocked by required conditions

This commit is contained in:
DESKTOP-RTLN3BA\$punk 2026-04-30 18:42:38 -07:00
parent 7aeb8bb0a8
commit c644f02d05
26 changed files with 346 additions and 380 deletions

View file

@ -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"
)

View file

@ -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:

View file

@ -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,

View file

@ -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",
},
],
)

View file

@ -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

View file

@ -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<void> {
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<Response>) => {
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<Response>) => {
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

View file

@ -132,8 +132,8 @@ export default function DesktopPermissionsPage() {
<div className="space-y-1">
<h1 className="text-2xl font-semibold tracking-tight">System Permissions</h1>
<p className="text-sm text-muted-foreground">
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.
</p>
</div>
</div>

View file

@ -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() {

View file

@ -182,11 +182,7 @@ const SurfsenseDocCitation: FC<{ chunkId: number }> = ({ chunkId }) => {
</p>
)}
{!isLoading && !error && citedChunk?.content && (
<MarkdownViewer
content={citedChunk.content}
maxLength={1500}
enableCitations
/>
<MarkdownViewer content={citedChunk.content} maxLength={1500} enableCitations />
)}
{!isLoading && !error && !citedChunk?.content && (
<p className="py-4 text-xs text-muted-foreground">No content available.</p>

View file

@ -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<PlateElementProps<MentionElementNode>> = ({ attributes, children, element }) => {
const MentionElement: FC<PlateElementProps<MentionElementNode>> = ({
attributes,
children,
element,
}) => {
const statusClass =
element.statusKind === "failed"
? "text-destructive"
@ -255,7 +265,10 @@ export const InlineMentionEditor = forwardRef<InlineMentionEditorRef, InlineMent
selection?.addRange(range);
}, []);
const getCurrentValue = useCallback(() => (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<InlineMentionEditorRef, InlineMent
const next = current.map((block) => {
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<InlineMentionEditorRef, InlineMent
removeDocumentChip,
setDocumentChipStatus,
}),
[clear, getMentionedDocs, getText, insertDocumentChip, removeDocumentChip, setDocumentChipStatus, setText]
[
clear,
getMentionedDocs,
getText,
insertDocumentChip,
removeDocumentChip,
setDocumentChipStatus,
setText,
]
);
const handleKeyDown = useCallback(
@ -488,14 +510,7 @@ export const InlineMentionEditor = forwardRef<InlineMentionEditorRef, InlineMent
removeDocumentChip(prev.id, prev.document_type);
onDocumentRemove?.(prev.id, prev.document_type);
},
[
editor.selection,
getCurrentValue,
onDocumentRemove,
onKeyDown,
onSubmit,
removeDocumentChip,
]
[editor.selection, getCurrentValue, onDocumentRemove, onKeyDown, onSubmit, removeDocumentChip]
);
const editableProps = useMemo(

View file

@ -12,14 +12,7 @@ import { ExternalLinkIcon } from "lucide-react";
import dynamic from "next/dynamic";
import { useParams } from "next/navigation";
import { useTheme } from "next-themes";
import {
createContext,
memo,
type ReactNode,
useCallback,
useContext,
useRef,
} from "react";
import { createContext, memo, type ReactNode, useCallback, useContext, useRef } from "react";
import rehypeKatex from "rehype-katex";
import remarkGfm from "remark-gfm";
import remarkMath from "remark-math";
@ -28,10 +21,6 @@ import { ImagePreview, ImageRoot, ImageZoom } from "@/components/assistant-ui/im
import "katex/dist/katex.min.css";
import { processChildrenWithCitations } from "@/components/citations/citation-renderer";
import { Skeleton } from "@/components/ui/skeleton";
import {
type CitationUrlMap,
preprocessCitationMarkdown,
} from "@/lib/citations/citation-parser";
import {
Table,
TableBody,
@ -41,6 +30,7 @@ import {
TableRow,
} from "@/components/ui/table";
import { useElectronAPI } from "@/hooks/use-platform";
import { type CitationUrlMap, preprocessCitationMarkdown } from "@/lib/citations/citation-parser";
import { cn } from "@/lib/utils";
function MarkdownCodeBlockSkeleton() {
@ -128,10 +118,7 @@ function preprocessMarkdown(content: string, urlMapRef: CitationUrlMapRef): stri
const MarkdownTextImpl = () => {
const urlMapRef = useRef<CitationUrlMap>(EMPTY_URL_MAP);
const preprocess = useCallback(
(content: string) => preprocessMarkdown(content, urlMapRef),
[]
);
const preprocess = useCallback((content: string) => preprocessMarkdown(content, urlMapRef), []);
return (
<CitationUrlMapContext.Provider value={urlMapRef}>
<MarkdownTextPrimitive
@ -334,10 +321,7 @@ const defaultComponents = memoizeMarkdownComponents({
const urlMap = useCitationUrlMap();
return (
<a
className={cn(
"aui-md-a font-medium text-primary underline underline-offset-4",
className
)}
className={cn("aui-md-a font-medium text-primary underline underline-offset-4", className)}
{...props}
>
{processChildrenWithCitations(children, urlMap)}

View file

@ -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">;

View file

@ -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";

View file

@ -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_<run_id>`` 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

View file

@ -64,9 +64,7 @@ export function processChildrenWithCitations(
return (
<span key={`citation-seg-${childIndex}`}>
{segments.map((segment) =>
typeof segment === "string"
? segment
: renderCitationToken(segment, ordinal++)
typeof segment === "string" ? segment : renderCitationToken(segment, ordinal++)
)}
</span>
);

View file

@ -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 `<a>` skip in
* `MarkdownViewer`.
*/
const SKIP_SUBTREE_TYPES = new Set<string>([
KEYS.codeBlock,
"code_line",
KEYS.link,
]);
const SKIP_SUBTREE_TYPES = new Set<string>([KEYS.codeBlock, "code_line", KEYS.link]);
/**
* Build the marks portion of a Slate text node so we can preserve formatting

View file

@ -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,

View file

@ -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";

View file

@ -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({

View file

@ -88,71 +88,68 @@ export function applyActionLogSse(
searchSpaceId,
event,
});
queryClient.setQueryData<AgentActionListResponse>(
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<AgentActionListResponse>(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<AgentActionListResponse>(
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<AgentActionListResponse>(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<AgentActionListResponse>(
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<AgentActionListResponse>(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<AgentActionListResponse>(
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<AgentActionListResponse>(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}`) ?? [];
},

View file

@ -53,7 +53,10 @@ function getErrorMessage(error: unknown): string {
}
}
function getErrorCode(error: unknown, parsedJson: Record<string, unknown> | null): string | undefined {
function getErrorCode(
error: unknown,
parsedJson: Record<string, unknown> | 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",

View file

@ -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 });
}

View file

@ -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) {

View file

@ -16,9 +16,7 @@ export type EditedInterruptAction = {
args: Record<string, unknown>;
};
function readInterruptActions(
interruptData: Record<string, unknown>
): InterruptActionRequest[] {
function readInterruptActions(interruptData: Record<string, unknown>): 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));
}

View file

@ -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;

View file

@ -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