mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-06-18 21:15:16 +02:00
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
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:
parent
7aeb8bb0a8
commit
c644f02d05
26 changed files with 346 additions and 380 deletions
|
|
@ -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"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
|
|
|
|||
|
|
@ -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">;
|
||||
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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}`) ?? [];
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue