feat: moved most things behind correct feature flag

This commit is contained in:
DESKTOP-RTLN3BA\$punk 2026-05-02 23:10:48 -07:00
parent bdb97a0888
commit c938d39277
13 changed files with 237 additions and 85 deletions

View file

@ -308,6 +308,24 @@ STT_SERVICE=local/base
# Advanced (optional)
# ------------------------------------------------------------------------------
# New-chat agent feature flags
SURFSENSE_ENABLE_CONTEXT_EDITING=true
SURFSENSE_ENABLE_COMPACTION_V2=true
SURFSENSE_ENABLE_RETRY_AFTER=true
SURFSENSE_ENABLE_MODEL_FALLBACK=false
SURFSENSE_ENABLE_MODEL_CALL_LIMIT=true
SURFSENSE_ENABLE_TOOL_CALL_LIMIT=true
SURFSENSE_ENABLE_TOOL_CALL_REPAIR=true
SURFSENSE_ENABLE_BUSY_MUTEX=true
SURFSENSE_ENABLE_SKILLS=true
SURFSENSE_ENABLE_SPECIALIZED_SUBAGENTS=true
SURFSENSE_ENABLE_KB_PLANNER_RUNNABLE=true
SURFSENSE_ENABLE_ACTION_LOG=true
SURFSENSE_ENABLE_REVERT_ROUTE=true
SURFSENSE_ENABLE_PERMISSION=true
SURFSENSE_ENABLE_DOOM_LOOP=true
SURFSENSE_ENABLE_STREAM_PARITY_V2=true
# Periodic connector sync interval (default: 5m)
# SCHEDULE_CHECKER_INTERVAL=5m

View file

@ -3,8 +3,10 @@ Feature flags for the SurfSense new_chat agent stack.
These flags gate the newer agent middleware (some ported from OpenCode,
some sourced from ``langchain.agents.middleware`` / ``deepagents``, some
SurfSense-native). They follow a "default-OFF for risky things,
default-ON for safe upgrades, master kill-switch for everything new" model.
SurfSense-native). Most shipped agent-stack upgrades default ON so Docker
image updates work even when older installs do not have newly introduced
environment variables. Risky/experimental integrations stay default OFF,
and the master kill-switch can still disable everything new.
All new middleware checks its flag at agent build time. If the master
kill-switch ``SURFSENSE_DISABLE_NEW_AGENT_STACK`` is set, every new
@ -14,16 +16,19 @@ operators a single switch to revert to pre-port behavior.
Examples
--------
Local development (recommended for trying everything except doom-loop / selector):
Defaults:
SURFSENSE_ENABLE_CONTEXT_EDITING=true
SURFSENSE_ENABLE_COMPACTION_V2=true
SURFSENSE_ENABLE_RETRY_AFTER=true
SURFSENSE_ENABLE_MODEL_FALLBACK=false
SURFSENSE_ENABLE_MODEL_CALL_LIMIT=true
SURFSENSE_ENABLE_TOOL_CALL_LIMIT=true
SURFSENSE_ENABLE_TOOL_CALL_REPAIR=true
SURFSENSE_ENABLE_PERMISSION=false # default off, opt-in per deploy
SURFSENSE_ENABLE_DOOM_LOOP=false # default off until UI ships
SURFSENSE_ENABLE_LLM_TOOL_SELECTOR=false
SURFSENSE_ENABLE_STREAM_PARITY_V2=false # structured streaming events
SURFSENSE_ENABLE_PERMISSION=true
SURFSENSE_ENABLE_DOOM_LOOP=true
SURFSENSE_ENABLE_LLM_TOOL_SELECTOR=false # adds a per-turn LLM call
SURFSENSE_ENABLE_STREAM_PARITY_V2=true
Master kill-switch (overrides everything else):
@ -60,32 +65,28 @@ class AgentFeatureFlags:
disable_new_agent_stack: bool = False
# Agent quality — context budget, retry/limits, name-repair, doom-loop
enable_context_editing: bool = False
enable_compaction_v2: bool = False
enable_retry_after: bool = False
enable_context_editing: bool = True
enable_compaction_v2: bool = True
enable_retry_after: bool = True
enable_model_fallback: bool = False
enable_model_call_limit: bool = False
enable_tool_call_limit: bool = False
enable_tool_call_repair: bool = False
enable_doom_loop: bool = (
False # Default OFF until UI handles permission='doom_loop'
)
enable_model_call_limit: bool = True
enable_tool_call_limit: bool = True
enable_tool_call_repair: bool = True
enable_doom_loop: bool = True
# Safety — permissions, concurrency, tool-set narrowing
enable_permission: bool = False # Default OFF for first deploy
enable_busy_mutex: bool = False
enable_permission: bool = True
enable_busy_mutex: bool = True
enable_llm_tool_selector: bool = False # Default OFF — adds per-turn LLM cost
# Skills + subagents
enable_skills: bool = False
enable_specialized_subagents: bool = False
enable_kb_planner_runnable: bool = False
enable_skills: bool = True
enable_specialized_subagents: bool = True
enable_kb_planner_runnable: bool = True
# Snapshot / revert
enable_action_log: bool = False
enable_revert_route: bool = (
False # Backend ships before UI; route returns 503 until this flips
)
enable_action_log: bool = True
enable_revert_route: bool = True
# Streaming parity v2 — opt in to LangChain's structured
# ``AIMessageChunk`` content (typed reasoning blocks, tool-input
@ -94,7 +95,7 @@ class AgentFeatureFlags:
# text path and the synthetic ``call_<run_id>`` tool-call id (no
# ``langchainToolCallId`` propagation). Schema migrations 135/136
# ship unconditionally because they're forward-compatible.
enable_stream_parity_v2: bool = False
enable_stream_parity_v2: bool = True
# Plugins
enable_plugin_loader: bool = False
@ -115,43 +116,64 @@ class AgentFeatureFlags:
"SURFSENSE_DISABLE_NEW_AGENT_STACK is set: every new agent "
"middleware is forced OFF for this build."
)
return cls(disable_new_agent_stack=True)
return cls(
disable_new_agent_stack=True,
enable_context_editing=False,
enable_compaction_v2=False,
enable_retry_after=False,
enable_model_fallback=False,
enable_model_call_limit=False,
enable_tool_call_limit=False,
enable_tool_call_repair=False,
enable_doom_loop=False,
enable_permission=False,
enable_busy_mutex=False,
enable_llm_tool_selector=False,
enable_skills=False,
enable_specialized_subagents=False,
enable_kb_planner_runnable=False,
enable_action_log=False,
enable_revert_route=False,
enable_stream_parity_v2=False,
enable_plugin_loader=False,
enable_otel=False,
)
return cls(
disable_new_agent_stack=False,
# Agent quality
enable_context_editing=_env_bool("SURFSENSE_ENABLE_CONTEXT_EDITING", False),
enable_compaction_v2=_env_bool("SURFSENSE_ENABLE_COMPACTION_V2", False),
enable_retry_after=_env_bool("SURFSENSE_ENABLE_RETRY_AFTER", False),
enable_context_editing=_env_bool("SURFSENSE_ENABLE_CONTEXT_EDITING", True),
enable_compaction_v2=_env_bool("SURFSENSE_ENABLE_COMPACTION_V2", True),
enable_retry_after=_env_bool("SURFSENSE_ENABLE_RETRY_AFTER", True),
enable_model_fallback=_env_bool("SURFSENSE_ENABLE_MODEL_FALLBACK", False),
enable_model_call_limit=_env_bool(
"SURFSENSE_ENABLE_MODEL_CALL_LIMIT", False
"SURFSENSE_ENABLE_MODEL_CALL_LIMIT", True
),
enable_tool_call_limit=_env_bool("SURFSENSE_ENABLE_TOOL_CALL_LIMIT", False),
enable_tool_call_limit=_env_bool("SURFSENSE_ENABLE_TOOL_CALL_LIMIT", True),
enable_tool_call_repair=_env_bool(
"SURFSENSE_ENABLE_TOOL_CALL_REPAIR", False
"SURFSENSE_ENABLE_TOOL_CALL_REPAIR", True
),
enable_doom_loop=_env_bool("SURFSENSE_ENABLE_DOOM_LOOP", False),
enable_doom_loop=_env_bool("SURFSENSE_ENABLE_DOOM_LOOP", True),
# Safety
enable_permission=_env_bool("SURFSENSE_ENABLE_PERMISSION", False),
enable_busy_mutex=_env_bool("SURFSENSE_ENABLE_BUSY_MUTEX", False),
enable_permission=_env_bool("SURFSENSE_ENABLE_PERMISSION", True),
enable_busy_mutex=_env_bool("SURFSENSE_ENABLE_BUSY_MUTEX", True),
enable_llm_tool_selector=_env_bool(
"SURFSENSE_ENABLE_LLM_TOOL_SELECTOR", False
),
# Skills + subagents
enable_skills=_env_bool("SURFSENSE_ENABLE_SKILLS", False),
enable_skills=_env_bool("SURFSENSE_ENABLE_SKILLS", True),
enable_specialized_subagents=_env_bool(
"SURFSENSE_ENABLE_SPECIALIZED_SUBAGENTS", False
"SURFSENSE_ENABLE_SPECIALIZED_SUBAGENTS", True
),
enable_kb_planner_runnable=_env_bool(
"SURFSENSE_ENABLE_KB_PLANNER_RUNNABLE", False
"SURFSENSE_ENABLE_KB_PLANNER_RUNNABLE", True
),
# Snapshot / revert
enable_action_log=_env_bool("SURFSENSE_ENABLE_ACTION_LOG", False),
enable_revert_route=_env_bool("SURFSENSE_ENABLE_REVERT_ROUTE", False),
enable_action_log=_env_bool("SURFSENSE_ENABLE_ACTION_LOG", True),
enable_revert_route=_env_bool("SURFSENSE_ENABLE_REVERT_ROUTE", True),
# Streaming parity v2
enable_stream_parity_v2=_env_bool(
"SURFSENSE_ENABLE_STREAM_PARITY_V2", False
"SURFSENSE_ENABLE_STREAM_PARITY_V2", True
),
# Plugins
enable_plugin_loader=_env_bool("SURFSENSE_ENABLE_PLUGIN_LOADER", False),

View file

@ -23,6 +23,7 @@ from fastapi import APIRouter, Depends
from pydantic import BaseModel
from app.agents.new_chat.feature_flags import AgentFeatureFlags, get_flags
from app.config import config
from app.db import User
from app.users import current_active_user
@ -58,10 +59,15 @@ class AgentFeatureFlagsRead(BaseModel):
enable_otel: bool
enable_desktop_local_filesystem: bool
@classmethod
def from_flags(cls, flags: AgentFeatureFlags) -> AgentFeatureFlagsRead:
# asdict() avoids missing-field bugs when AgentFeatureFlags grows.
return cls(**asdict(flags))
return cls(
**asdict(flags),
enable_desktop_local_filesystem=config.ENABLE_DESKTOP_LOCAL_FILESYSTEM,
)
@router.get("/agent/flags", response_model=AgentFeatureFlagsRead)

View file

@ -399,7 +399,7 @@ async def resolve_or_get_pinned_llm_config_id(
False if force_repin_free else await _is_premium_eligible(session, user_id)
)
if premium_eligible:
eligible = candidates
eligible = [c for c in candidates if _tier_of(c) == "premium"]
else:
eligible = [c for c in candidates if _tier_of(c) != "premium"]

View file

@ -31,18 +31,38 @@ def _clear_all(monkeypatch: pytest.MonkeyPatch) -> None:
"SURFSENSE_ENABLE_KB_PLANNER_RUNNABLE",
"SURFSENSE_ENABLE_ACTION_LOG",
"SURFSENSE_ENABLE_REVERT_ROUTE",
"SURFSENSE_ENABLE_STREAM_PARITY_V2",
"SURFSENSE_ENABLE_PLUGIN_LOADER",
"SURFSENSE_ENABLE_OTEL",
]:
monkeypatch.delenv(name, raising=False)
def test_defaults_all_off(monkeypatch: pytest.MonkeyPatch) -> None:
def test_defaults_match_shipped_agent_stack(monkeypatch: pytest.MonkeyPatch) -> None:
_clear_all(monkeypatch)
flags = reload_for_tests()
assert isinstance(flags, AgentFeatureFlags)
assert flags.disable_new_agent_stack is False
assert flags.any_new_middleware_enabled() is False
assert flags.enable_context_editing is True
assert flags.enable_compaction_v2 is True
assert flags.enable_retry_after is True
assert flags.enable_model_fallback is False
assert flags.enable_model_call_limit is True
assert flags.enable_tool_call_limit is True
assert flags.enable_tool_call_repair is True
assert flags.enable_doom_loop is True
assert flags.enable_permission is True
assert flags.enable_busy_mutex is True
assert flags.enable_llm_tool_selector is False
assert flags.enable_skills is True
assert flags.enable_specialized_subagents is True
assert flags.enable_kb_planner_runnable is True
assert flags.enable_action_log is True
assert flags.enable_revert_route is True
assert flags.enable_stream_parity_v2 is True
assert flags.enable_plugin_loader is False
assert flags.enable_otel is False
assert flags.any_new_middleware_enabled() is True
def test_master_kill_switch_overrides_individual_flags(
@ -100,21 +120,13 @@ def test_each_flag_can_be_set_independently(monkeypatch: pytest.MonkeyPatch) ->
"enable_kb_planner_runnable": "SURFSENSE_ENABLE_KB_PLANNER_RUNNABLE",
"enable_action_log": "SURFSENSE_ENABLE_ACTION_LOG",
"enable_revert_route": "SURFSENSE_ENABLE_REVERT_ROUTE",
"enable_stream_parity_v2": "SURFSENSE_ENABLE_STREAM_PARITY_V2",
"enable_plugin_loader": "SURFSENSE_ENABLE_PLUGIN_LOADER",
"enable_otel": "SURFSENSE_ENABLE_OTEL",
}
# `enable_otel` is intentionally orthogonal — it does NOT count toward
# ``any_new_middleware_enabled`` because OTel is observability-only and
# ships under its own ``OTEL_EXPORTER_OTLP_ENDPOINT`` requirement.
counts_toward_middleware = {k for k in flag_to_env if k != "enable_otel"}
for attr, env_name in flag_to_env.items():
_clear_all(monkeypatch)
monkeypatch.setenv(env_name, "true")
monkeypatch.setenv(env_name, "false")
flags = reload_for_tests()
assert getattr(flags, attr) is True, f"{attr} did not flip on for {env_name}"
if attr in counts_toward_middleware:
assert flags.any_new_middleware_enabled() is True
else:
assert flags.any_new_middleware_enabled() is False
assert getattr(flags, attr) is False, f"{attr} did not flip off for {env_name}"

View file

@ -101,11 +101,58 @@ async def test_auto_first_turn_pins_one_model(monkeypatch):
user_id="00000000-0000-0000-0000-000000000001",
selected_llm_config_id=0,
)
assert result.resolved_llm_config_id in {-1, -2}
assert result.resolved_llm_config_id == -1
assert session.thread.pinned_llm_config_id == result.resolved_llm_config_id
assert session.commit_count == 1
@pytest.mark.asyncio
async def test_premium_eligible_auto_prefers_premium_over_free(monkeypatch):
from app.config import config
session = _FakeSession(_thread())
monkeypatch.setattr(
config,
"GLOBAL_LLM_CONFIGS",
[
{
"id": -2,
"provider": "OPENAI",
"model_name": "gpt-free",
"api_key": "k1",
"billing_tier": "free",
"quality_score": 100,
},
{
"id": -1,
"provider": "OPENAI",
"model_name": "gpt-prem",
"api_key": "k2",
"billing_tier": "premium",
"quality_score": 10,
},
],
)
async def _allowed(*_args, **_kwargs):
return _FakeQuotaResult(allowed=True)
monkeypatch.setattr(
"app.services.auto_model_pin_service.TokenQuotaService.premium_get_usage",
_allowed,
)
result = await resolve_or_get_pinned_llm_config_id(
session,
thread_id=1,
search_space_id=10,
user_id="00000000-0000-0000-0000-000000000001",
selected_llm_config_id=0,
)
assert result.resolved_llm_config_id == -1
assert result.resolved_tier == "premium"
@pytest.mark.asyncio
async def test_next_turn_reuses_existing_pin(monkeypatch):
from app.config import config
@ -361,12 +408,12 @@ async def test_invalid_pinned_config_repairs_with_new_pin(monkeypatch):
],
)
async def _allowed(*_args, **_kwargs):
return _FakeQuotaResult(allowed=True)
async def _blocked(*_args, **_kwargs):
return _FakeQuotaResult(allowed=False)
monkeypatch.setattr(
"app.services.auto_model_pin_service.TokenQuotaService.premium_get_usage",
_allowed,
_blocked,
)
result = await resolve_or_get_pinned_llm_config_id(

View file

@ -5,7 +5,7 @@ import { BreadcrumbNav } from "@/components/seo/breadcrumb-nav";
export const metadata: Metadata = {
title: "Pricing | SurfSense - Free AI Search Plans",
description:
"Explore SurfSense plans and pricing. Start free with 500 pages & $5 of premium credit. Use ChatGPT, Claude AI, and premium AI models. Pay as you go at provider cost — $1 buys $1 of credit.",
"Explore SurfSense plans and pricing. Start free with 500 pages & $5 in premium credits. Use ChatGPT, Claude AI, and premium AI models. Pay as you go at provider cost.",
alternates: {
canonical: "https://surfsense.com/pricing",
},

View file

@ -13,6 +13,7 @@ import { useParams } from "next/navigation";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { toast } from "sonner";
import { z } from "zod";
import { agentFlagsAtom } from "@/atoms/agent/agent-flags-query.atom";
import { disabledToolsAtom } from "@/atoms/agent-tools/agent-tools.atoms";
import {
clearTargetCommentIdAtom,
@ -393,6 +394,8 @@ export default function NewChatPage() {
// Get current user for author info in shared chats
const { data: currentUser } = useAtomValue(currentUserAtom);
const { data: agentFlags } = useAtomValue(agentFlagsAtom);
const localFilesystemEnabled = agentFlags?.enable_desktop_local_filesystem === true;
// Live collaboration: sync session state and messages via Zero
useChatSessionStateSync(threadId);
@ -989,7 +992,9 @@ export default function NewChatPage() {
try {
const backendUrl = process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL || "http://localhost:8000";
const selection = await getAgentFilesystemSelection(searchSpaceId);
const selection = await getAgentFilesystemSelection(searchSpaceId, {
localFilesystemEnabled,
});
if (
selection.filesystem_mode === "desktop_local_folder" &&
(!selection.local_filesystem_mounts || selection.local_filesystem_mounts.length === 0)
@ -1311,6 +1316,7 @@ export default function NewChatPage() {
setAgentCreatedDocuments,
queryClient,
currentUser,
localFilesystemEnabled,
disabledTools,
updateChatTabTitle,
tokenUsageStore,
@ -1413,7 +1419,9 @@ export default function NewChatPage() {
try {
const backendUrl = process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL || "http://localhost:8000";
const selection = await getAgentFilesystemSelection(searchSpaceId);
const selection = await getAgentFilesystemSelection(searchSpaceId, {
localFilesystemEnabled,
});
const response = await fetchWithTurnCancellingRetry(() =>
fetch(`${backendUrl}/api/v1/threads/${resumeThreadId}/resume`, {
method: "POST",
@ -1561,6 +1569,7 @@ export default function NewChatPage() {
pendingInterrupt,
messages,
searchSpaceId,
localFilesystemEnabled,
queryClient,
tokenUsageStore,
fetchWithTurnCancellingRetry,
@ -1746,7 +1755,9 @@ export default function NewChatPage() {
? messageDocumentsMap[sourceUserMessageId]
: [];
try {
const selection = await getAgentFilesystemSelection(searchSpaceId);
const selection = await getAgentFilesystemSelection(searchSpaceId, {
localFilesystemEnabled,
});
const requestBody: Record<string, unknown> = {
search_space_id: searchSpaceId,
user_query: newUserQuery,
@ -2016,6 +2027,7 @@ export default function NewChatPage() {
searchSpaceId,
messages,
disabledTools,
localFilesystemEnabled,
messageDocumentsMap,
setMessageDocumentsMap,
queryClient,

View file

@ -178,6 +178,19 @@ const FLAG_GROUPS: FlagGroup[] = [
},
],
},
{
id: "desktop",
title: "Desktop",
subtitle: "Desktop-only capabilities exposed by the backend deployment.",
flags: [
{
key: "enable_desktop_local_filesystem",
label: "Local filesystem",
description: "Allow Desktop chat sessions to operate directly on selected local folders.",
envVar: "ENABLE_DESKTOP_LOCAL_FILESYSTEM",
},
],
},
];
function FlagRow({ def, value }: { def: FlagDef; value: boolean }) {

View file

@ -23,6 +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 { agentFlagsAtom } from "@/atoms/agent/agent-flags-query.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";
@ -197,6 +198,7 @@ function AuthenticatedDocumentsSidebarBase({
const setConnectorDialogOpen = useSetAtom(connectorDialogOpenAtom);
const setRightPanelCollapsed = useSetAtom(rightPanelCollapsedAtom);
const openEditorPanel = useSetAtom(openEditorPanelAtom);
const { data: agentFlags } = useAtomValue(agentFlagsAtom);
const { data: connectors } = useAtomValue(connectorsAtom);
const connectorCount = connectors?.length ?? 0;
@ -209,6 +211,7 @@ function AuthenticatedDocumentsSidebarBase({
const [watchedFolderIds, setWatchedFolderIds] = useState<Set<number>>(new Set());
const [folderWatchOpen, setFolderWatchOpen] = useAtom(folderWatchDialogOpenAtom);
const [watchInitialFolder, setWatchInitialFolder] = useAtom(folderWatchInitialFolderAtom);
const localFilesystemEnabled = agentFlags?.enable_desktop_local_filesystem === true;
const isElectron =
desktopFeaturesEnabled && typeof window !== "undefined" && !!window.electronAPI;
@ -1036,9 +1039,12 @@ function AuthenticatedDocumentsSidebarBase({
return () => document.removeEventListener("keydown", handleEscape);
}, [open, onOpenChange, isMobile, setRightPanelCollapsed]);
const showFilesystemTabs = !isMobile && !!electronAPI && !!filesystemSettings;
const showFilesystemTabs =
!isMobile && !!electronAPI && !!filesystemSettings && localFilesystemEnabled;
const currentFilesystemTab =
filesystemSettings?.mode === "desktop_local_folder" ? "local" : "cloud";
localFilesystemEnabled && filesystemSettings?.mode === "desktop_local_folder"
? "local"
: "cloud";
const showCloudSkeleton =
currentFilesystemTab === "cloud" &&
(zeroFoldersResult.type !== "complete" || zeroAllDocsResult.type !== "complete");

View file

@ -12,11 +12,11 @@ const demoPlans = [
price: "0",
yearlyPrice: "0",
period: "",
billingText: "500 pages + $5 of premium credit included",
billingText: "500 pages + $5 in premium credits included",
features: [
"Self Hostable",
"500 pages included to start",
"$5 of premium credit to start, billed at provider cost",
"$5 in premium credits for paid AI models and premium AI features",
"Includes access to OpenAI text, audio and image models",
"Realtime Collaborative Group Chats with teammates",
"Community support on Discord",
@ -35,7 +35,7 @@ const demoPlans = [
features: [
"Everything in Free",
"Buy 1,000-page packs at $1 each",
"Top up premium credit at $1 per $1 of credit, billed at provider cost",
"Top up premium credits at $1 per $1 of credit, billed at provider cost",
"Use premium AI models like GPT-5.4, Claude Sonnet 4.6, Gemini 2.5 Pro & 100+ more via OpenRouter",
"Priority support on Discord",
],
@ -89,7 +89,7 @@ const faqData: FAQSection[] = [
{
question: "What are Basic and Premium processing modes?",
answer:
"When uploading documents, you can choose between two processing modes. Basic mode uses standard extraction and costs 1 page credit per page, great for most documents. Premium mode uses advanced extraction optimized for complex financial, medical, and legal documents with intricate tables, layouts, and formatting. Premium costs 10 page credits per page but delivers significantly higher fidelity output for these specialized document types.",
"When uploading documents, you can choose between two processing modes. Basic mode uses standard extraction and costs 1 page credit per page, great for most documents. Premium processing mode uses advanced extraction optimized for complex financial, medical, and legal documents with intricate tables, layouts, and formatting. It costs 10 page credits per page and does not use your premium AI credits.",
},
{
question: "How does the Pay As You Go plan work?",
@ -129,27 +129,32 @@ const faqData: FAQSection[] = [
],
},
{
title: "Premium Credit",
title: "Premium Credits",
items: [
{
question: 'What is "premium credit"?',
question: 'What are "premium credits"?',
answer:
"Premium credit is your USD balance for using premium AI models like GPT-5.4, Claude Sonnet 4.6, and Gemini 2.5 Pro in SurfSense. Each AI request debits the actual USD cost the provider charges, so cheap and expensive models bill proportionally. Non-premium models (such as the free-tier models available without login) don't touch your premium credit.",
"Premium credits are your USD balance for paid AI usage in SurfSense, including premium AI models like GPT-5.4, Claude Sonnet 4.6, and Gemini 2.5 Pro, plus premium AI features such as image generation, podcasts, and video presentations when they use paid models. Each request debits the actual USD provider cost, so cheaper and more expensive models bill proportionally.",
},
{
question: "How much premium credit do I get for free?",
question: "How many premium credits do I get for free?",
answer:
"Every registered SurfSense account starts with $5 of premium credit at no cost. Anonymous users (no login) get 500,000 free tokens across all free models. Once your free credit runs out, you can top up at any time.",
"Every registered SurfSense account starts with $5 in premium credits at no cost. Anonymous users (no login) get 500,000 free tokens across free models before creating an account. Once your included premium credits run out, you can top up at any time.",
},
{
question: "How does buying premium credit work?",
question: "How does buying premium credits work?",
answer:
"Just like pages, there's no subscription. Top-ups buy $1 of credit for $1 — every cent you pay is spent at provider cost, no markup. Purchased credit is added to your account immediately. You can buy up to $100 at a time.",
"Premium credit top-ups are pay as you go, with no subscription. $1 buys $1 of credit, and your balance is spent at provider cost. Purchased credit is added to your account immediately. You can buy up to $100 at a time.",
},
{
question: "What happens if I run out of premium credit?",
question: "Are premium credits the same as page credits?",
answer:
"When your premium credit balance runs low (below 20%), you'll see a warning. Once you run out, premium model requests are paused until you top up. You can always switch to non-premium models, which don't touch your premium credit.",
"No. Page credits pay for document indexing and file-based connector processing. Premium credits pay for paid AI usage, such as premium model chats and premium AI generation features. Premium document processing mode sounds similar, but it consumes page credits, not premium credits.",
},
{
question: "What happens if I run out of premium credits?",
answer:
"When your premium credit balance runs low, you'll see a warning. Once you run out, paid model requests and premium AI features are paused until you top up. You can still use non-premium models and features that do not consume premium credits.",
},
],
},
@ -159,7 +164,7 @@ const faqData: FAQSection[] = [
{
question: "Can I self-host SurfSense with unlimited pages and credit?",
answer:
"Yes! When self-hosting, you have full control over your page and premium-credit limits. The default self-hosted setup gives you effectively unlimited pages and premium credit, so you can index as much data and use as many AI queries as your infrastructure supports.",
"Yes! When self-hosting, you have full control over your page and premium credit limits. The default self-hosted setup gives you effectively unlimited pages and premium credits, so you can index as much data and use as many AI queries as your infrastructure supports.",
},
],
},
@ -250,7 +255,7 @@ function PricingFAQ() {
Frequently Asked Questions
</h2>
<p className="mx-auto mt-4 max-w-2xl text-lg text-muted-foreground">
Everything you need to know about SurfSense pages, premium credit, and billing. Can&apos;t
Everything you need to know about SurfSense pages, premium credits, and billing. Can&apos;t
find what you need? Reach out at{" "}
<a href="mailto:rohan@surfsense.com" className="text-blue-500 underline">
rohan@surfsense.com
@ -335,7 +340,7 @@ function PricingBasic() {
<Pricing
plans={demoPlans}
title="SurfSense Pricing"
description="Start free with 500 pages & $5 of premium credit. Pay as you go, billed at provider cost."
description="Start free with 500 pages & $5 in premium credits. Pay as you go."
/>
<PricingFAQ />
</>

View file

@ -12,6 +12,10 @@ export interface AgentFilesystemSelection {
local_filesystem_mounts?: AgentFilesystemMountSelection[];
}
export interface AgentFilesystemSelectionOptions {
localFilesystemEnabled: boolean;
}
const DEFAULT_SELECTION: AgentFilesystemSelection = {
filesystem_mode: "cloud",
client_platform: "web",
@ -23,10 +27,15 @@ export function getClientPlatform(): ClientPlatform {
}
export async function getAgentFilesystemSelection(
searchSpaceId?: number | null
searchSpaceId?: number | null,
options?: AgentFilesystemSelectionOptions
): Promise<AgentFilesystemSelection> {
const platform = getClientPlatform();
if (platform !== "desktop" || !window.electronAPI?.getAgentFilesystemSettings) {
if (
platform !== "desktop" ||
!options?.localFilesystemEnabled ||
!window.electronAPI?.getAgentFilesystemSettings
) {
return { ...DEFAULT_SELECTION, client_platform: platform };
}
try {

View file

@ -27,6 +27,8 @@ const AgentFeatureFlagsSchema = z.object({
enable_plugin_loader: z.boolean(),
enable_otel: z.boolean(),
enable_desktop_local_filesystem: z.boolean(),
});
export type AgentFeatureFlags = z.infer<typeof AgentFeatureFlagsSchema>;