diff --git a/surfsense_backend/app/agents/autocomplete/autocomplete_agent.py b/surfsense_backend/app/agents/autocomplete/autocomplete_agent.py index 928a133cc..c6a071b0f 100644 --- a/surfsense_backend/app/agents/autocomplete/autocomplete_agent.py +++ b/surfsense_backend/app/agents/autocomplete/autocomplete_agent.py @@ -16,7 +16,8 @@ from __future__ import annotations import asyncio import logging import uuid -from typing import Any, AsyncGenerator +from collections.abc import AsyncGenerator +from typing import Any from deepagents.graph import BASE_AGENT_PROMPT from deepagents.middleware.patch_tool_calls import PatchToolCallsMiddleware @@ -122,6 +123,7 @@ def _build_autocomplete_system_prompt(app_name: str, window_title: str) -> str: class _KBResult: """Container for pre-computed KB filesystem results.""" + __slots__ = ("files", "ls_ai_msg", "ls_tool_msg") def __init__( @@ -171,13 +173,16 @@ async def precompute_kb_filesystem( return _KBResult() doc_paths = [ - p for p, v in new_files.items() + p + for p, v in new_files.items() if p.startswith("/documents/") and v is not None ] tool_call_id = f"auto_ls_{uuid.uuid4().hex[:12]}" ai_msg = AIMessage( content="", - tool_calls=[{"name": "ls", "args": {"path": "/documents"}, "id": tool_call_id}], + tool_calls=[ + {"name": "ls", "args": {"path": "/documents"}, "id": tool_call_id} + ], ) tool_msg = ToolMessage( content=str(doc_paths) if doc_paths else "No documents found.", @@ -186,7 +191,9 @@ async def precompute_kb_filesystem( return _KBResult(files=new_files, ls_ai_msg=ai_msg, ls_tool_msg=tool_msg) except Exception: - logger.warning("KB pre-computation failed, proceeding without KB", exc_info=True) + logger.warning( + "KB pre-computation failed, proceeding without KB", exc_info=True + ) return _KBResult() @@ -320,7 +327,9 @@ async def stream_autocomplete_agent( ) try: - async for event in agent.astream_events(input_data, config=config, version="v2"): + async for event in agent.astream_events( + input_data, config=config, version="v2" + ): event_type = event.get("event", "") if event_type == "on_chat_model_stream": @@ -338,7 +347,9 @@ async def stream_autocomplete_agent( yield step_event current_text_id = streaming_service.generate_text_id() yield streaming_service.format_text_start(current_text_id) - yield streaming_service.format_text_delta(current_text_id, content) + yield streaming_service.format_text_delta( + current_text_id, content + ) elif event_type == "on_tool_start": active_tool_depth += 1 @@ -425,5 +436,7 @@ def _describe_tool_call(tool_name: str, tool_input: Any) -> tuple[str, list[str] pat = inp.get("pattern", "") path = inp.get("path", "") display_pat = pat[:60] + ("…" if len(pat) > 60 else "") - return "Searching content", [f'"{display_pat}"' + (f" in {path}" if path else "")] + return "Searching content", [ + f'"{display_pat}"' + (f" in {path}" if path else "") + ] return f"Using {tool_name}", [] diff --git a/surfsense_backend/app/services/vision_autocomplete_service.py b/surfsense_backend/app/services/vision_autocomplete_service.py index 2c2cd65d2..c28962b31 100644 --- a/surfsense_backend/app/services/vision_autocomplete_service.py +++ b/surfsense_backend/app/services/vision_autocomplete_service.py @@ -98,7 +98,9 @@ async def stream_vision_autocomplete( step_id=PREP_STEP_ID, title="Searching knowledge base", status="complete", - items=[f"Found {doc_count} document{'s' if doc_count != 1 else ''}"] if kb_query else ["Skipped"], + items=[f"Found {doc_count} document{'s' if doc_count != 1 else ''}"] + if kb_query + else ["Skipped"], ) # Build agent input with pre-computed KB as initial state @@ -116,24 +118,33 @@ async def stream_vision_autocomplete( "for the active text area based on what you see." ) - user_message = HumanMessage(content=[ - {"type": "text", "text": instruction}, - {"type": "image_url", "image_url": {"url": screenshot_data_url}}, - ]) + user_message = HumanMessage( + content=[ + {"type": "text", "text": instruction}, + {"type": "image_url", "image_url": {"url": screenshot_data_url}}, + ] + ) input_data: dict = {"messages": [user_message]} if has_kb: input_data["files"] = kb.files input_data["messages"] = [kb.ls_ai_msg, kb.ls_tool_msg, user_message] - logger.info("Autocomplete: injected %d KB files into agent initial state", doc_count) + logger.info( + "Autocomplete: injected %d KB files into agent initial state", doc_count + ) else: - logger.info("Autocomplete: no KB documents found, proceeding with screenshot only") + logger.info( + "Autocomplete: no KB documents found, proceeding with screenshot only" + ) # Stream the agent (message_start already sent above) try: async for sse in stream_autocomplete_agent( - agent, input_data, streaming, emit_message_start=False, + agent, + input_data, + streaming, + emit_message_start=False, ): yield sse except Exception as e: diff --git a/surfsense_web/app/dashboard/[search_space_id]/user-settings/components/DesktopContent.tsx b/surfsense_web/app/dashboard/[search_space_id]/user-settings/components/DesktopContent.tsx index 07b746a19..a2f9da0f8 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/user-settings/components/DesktopContent.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/user-settings/components/DesktopContent.tsx @@ -3,10 +3,7 @@ import { Clipboard, Sparkles } from "lucide-react"; import { useCallback, useEffect, useState } from "react"; import { toast } from "sonner"; -import { - DEFAULT_SHORTCUTS, - ShortcutRecorder, -} from "@/components/desktop/shortcut-recorder"; +import { DEFAULT_SHORTCUTS, ShortcutRecorder } from "@/components/desktop/shortcut-recorder"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Label } from "@/components/ui/label"; import { Spinner } from "@/components/ui/spinner"; @@ -29,22 +26,23 @@ export function DesktopContent() { let mounted = true; - Promise.all([ - api.getAutocompleteEnabled(), - api.getShortcuts?.() ?? Promise.resolve(null), - ]).then(([autoEnabled, config]) => { - if (!mounted) return; - setEnabled(autoEnabled); - if (config) setShortcuts(config); - setLoading(false); - setShortcutsLoaded(true); - }).catch(() => { - if (!mounted) return; - setLoading(false); - setShortcutsLoaded(true); - }); + Promise.all([api.getAutocompleteEnabled(), api.getShortcuts?.() ?? Promise.resolve(null)]) + .then(([autoEnabled, config]) => { + if (!mounted) return; + setEnabled(autoEnabled); + if (config) setShortcuts(config); + setLoading(false); + setShortcutsLoaded(true); + }) + .catch(() => { + if (!mounted) return; + setLoading(false); + setShortcutsLoaded(true); + }); - return () => { mounted = false; }; + return () => { + mounted = false; + }; }, [api]); if (!api) { diff --git a/surfsense_web/app/dashboard/layout.tsx b/surfsense_web/app/dashboard/layout.tsx index 25bea5467..1f5481b15 100644 --- a/surfsense_web/app/dashboard/layout.tsx +++ b/surfsense_web/app/dashboard/layout.tsx @@ -3,7 +3,7 @@ import { useEffect, useState } from "react"; import { USER_QUERY_KEY } from "@/atoms/user/user-query.atoms"; import { useGlobalLoadingEffect } from "@/hooks/use-global-loading"; -import { getBearerToken, ensureTokensFromElectron, redirectToLogin } from "@/lib/auth-utils"; +import { ensureTokensFromElectron, getBearerToken, redirectToLogin } from "@/lib/auth-utils"; import { queryClient } from "@/lib/query-client/client"; interface DashboardLayoutProps { diff --git a/surfsense_web/app/desktop/login/page.tsx b/surfsense_web/app/desktop/login/page.tsx index 529577b59..c81e284ba 100644 --- a/surfsense_web/app/desktop/login/page.tsx +++ b/surfsense_web/app/desktop/login/page.tsx @@ -2,30 +2,15 @@ import { IconBrandGoogleFilled } from "@tabler/icons-react"; import { useAtom } from "jotai"; -import { - Eye, - EyeOff, - Keyboard, - Clipboard, - Sparkles, -} from "lucide-react"; +import { Clipboard, Eye, EyeOff, Keyboard, Sparkles } from "lucide-react"; import Image from "next/image"; import { useRouter } from "next/navigation"; import { useCallback, useEffect, useState } from "react"; import { toast } from "sonner"; import { loginMutationAtom } from "@/atoms/auth/auth-mutation.atoms"; -import { - DEFAULT_SHORTCUTS, - ShortcutRecorder, -} from "@/components/desktop/shortcut-recorder"; +import { DEFAULT_SHORTCUTS, ShortcutRecorder } from "@/components/desktop/shortcut-recorder"; import { Button } from "@/components/ui/button"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Separator } from "@/components/ui/separator"; @@ -38,8 +23,7 @@ const isGoogleAuth = AUTH_TYPE === "GOOGLE"; export default function DesktopLoginPage() { const router = useRouter(); const api = useElectronAPI(); - const [{ mutateAsync: login, isPending: isLoggingIn }] = - useAtom(loginMutationAtom); + const [{ mutateAsync: login, isPending: isLoggingIn }] = useAtom(loginMutationAtom); const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); @@ -54,10 +38,13 @@ export default function DesktopLoginPage() { setShortcutsLoaded(true); return; } - api.getShortcuts().then((config) => { - if (config) setShortcuts(config); - setShortcutsLoaded(true); - }).catch(() => setShortcutsLoaded(true)); + api + .getShortcuts() + .then((config) => { + if (config) setShortcuts(config); + setShortcutsLoaded(true); + }) + .catch(() => setShortcutsLoaded(true)); }, [api]); const updateShortcut = useCallback( @@ -118,8 +105,7 @@ export default function DesktopLoginPage() {
@@ -135,9 +121,7 @@ export default function DesktopLoginPage() { priority /> Welcome to SurfSense Desktop App - - Configure your shortcuts, then sign in to get started. - + Configure your shortcuts, then sign in to get started. @@ -181,11 +165,7 @@ export default function DesktopLoginPage() { {/* ---- Auth Section (second) ---- */} {isGoogleAuth ? ( - @@ -230,11 +210,7 @@ export default function DesktopLoginPage() { className="absolute inset-y-0 right-0 flex items-center pr-3 text-muted-foreground hover:text-foreground" tabIndex={-1} > - {showPassword ? ( - - ) : ( - - )} + {showPassword ? : } diff --git a/surfsense_web/app/desktop/permissions/page.tsx b/surfsense_web/app/desktop/permissions/page.tsx index b636fcd7c..a2fadc8ff 100644 --- a/surfsense_web/app/desktop/permissions/page.tsx +++ b/surfsense_web/app/desktop/permissions/page.tsx @@ -80,7 +80,9 @@ export default function DesktopPermissionsPage() { poll(); interval = setInterval(poll, 2000); - return () => { if (interval) clearInterval(interval); }; + return () => { + if (interval) clearInterval(interval); + }; }, [api]); if (!api) { @@ -204,6 +206,7 @@ export default function DesktopPermissionsPage() { Grant permissions to continue + {item.featured && ( + + + + + + + Desktop app only + + )} + {index !== TAB_ITEMS.length - 1 && (
)} @@ -263,13 +254,13 @@ const BrowserWindow = () => {

- {/* biome-ignore lint/a11y/useKeyWithClickEvents: wrapper for video expand */} -
-
+ @@ -277,11 +268,7 @@ const BrowserWindow = () => { {expanded && ( - + )} @@ -297,7 +284,7 @@ const TabVideo = memo(function TabVideo({ src }: { src: string }) { const video = videoRef.current; if (!video) return; video.currentTime = 0; - video.play().catch(() => { }); + video.play().catch(() => {}); }, [src]); const handleCanPlay = useCallback(() => { @@ -324,8 +311,7 @@ const TabVideo = memo(function TabVideo({ src }: { src: string }) { ); }); -const GITHUB_RELEASES_URL = - "https://github.com/MODSetter/SurfSense/releases/latest"; +const GITHUB_RELEASES_URL = "https://github.com/MODSetter/SurfSense/releases/latest"; const DownloadApp = memo(function DownloadApp() { return ( @@ -340,7 +326,16 @@ const DownloadApp = memo(function DownloadApp() { rel="noopener noreferrer" className="flex items-center gap-2 rounded-lg border border-neutral-200 bg-white px-4 py-2.5 text-sm font-medium text-neutral-700 shadow-sm transition hover:bg-neutral-50 dark:border-neutral-700 dark:bg-neutral-900 dark:text-neutral-200 dark:hover:bg-neutral-800" > - + @@ -353,7 +348,16 @@ const DownloadApp = memo(function DownloadApp() { rel="noopener noreferrer" className="flex items-center gap-2 rounded-lg border border-neutral-200 bg-white px-4 py-2.5 text-sm font-medium text-neutral-700 shadow-sm transition hover:bg-neutral-50 dark:border-neutral-700 dark:bg-neutral-900 dark:text-neutral-200 dark:hover:bg-neutral-800" > - + @@ -366,7 +370,16 @@ const DownloadApp = memo(function DownloadApp() { rel="noopener noreferrer" className="flex items-center gap-2 rounded-lg border border-neutral-200 bg-white px-4 py-2.5 text-sm font-medium text-neutral-700 shadow-sm transition hover:bg-neutral-50 dark:border-neutral-700 dark:bg-neutral-900 dark:text-neutral-200 dark:hover:bg-neutral-800" > - + diff --git a/surfsense_web/components/layout/ui/sidebar/DocumentsSidebar.tsx b/surfsense_web/components/layout/ui/sidebar/DocumentsSidebar.tsx index 35489fe32..6de235d17 100644 --- a/surfsense_web/components/layout/ui/sidebar/DocumentsSidebar.tsx +++ b/surfsense_web/components/layout/ui/sidebar/DocumentsSidebar.tsx @@ -302,24 +302,27 @@ export function DocumentsSidebar({ [searchSpaceId, electronAPI] ); - const handleStopWatching = useCallback(async (folder: FolderDisplay) => { - if (!electronAPI) return; + const handleStopWatching = useCallback( + async (folder: FolderDisplay) => { + if (!electronAPI) return; - const watchedFolders = await electronAPI.getWatchedFolders(); - const matched = watchedFolders.find((wf) => wf.rootFolderId === folder.id); - if (!matched) { - toast.error("This folder is not being watched"); - return; - } + const watchedFolders = await electronAPI.getWatchedFolders(); + const matched = watchedFolders.find((wf) => wf.rootFolderId === folder.id); + if (!matched) { + toast.error("This folder is not being watched"); + return; + } - await electronAPI.removeWatchedFolder(matched.path); - try { - await foldersApiService.stopWatching(folder.id); - } catch (err) { - console.error("[DocumentsSidebar] Failed to clear watched metadata:", err); - } - toast.success(`Stopped watching: ${matched.name}`); - }, [electronAPI]); + await electronAPI.removeWatchedFolder(matched.path); + try { + await foldersApiService.stopWatching(folder.id); + } catch (err) { + console.error("[DocumentsSidebar] Failed to clear watched metadata:", err); + } + toast.success(`Stopped watching: ${matched.name}`); + }, + [electronAPI] + ); const handleRenameFolder = useCallback(async (folder: FolderDisplay, newName: string) => { try { @@ -330,22 +333,25 @@ export function DocumentsSidebar({ } }, []); - const handleDeleteFolder = useCallback(async (folder: FolderDisplay) => { - if (!confirm(`Delete folder "${folder.name}" and all its contents?`)) return; - try { - if (electronAPI) { - const watchedFolders = await electronAPI.getWatchedFolders(); - const matched = watchedFolders.find((wf) => wf.rootFolderId === folder.id); - if (matched) { - await electronAPI.removeWatchedFolder(matched.path); + const handleDeleteFolder = useCallback( + async (folder: FolderDisplay) => { + if (!confirm(`Delete folder "${folder.name}" and all its contents?`)) return; + try { + if (electronAPI) { + const watchedFolders = await electronAPI.getWatchedFolders(); + const matched = watchedFolders.find((wf) => wf.rootFolderId === folder.id); + if (matched) { + await electronAPI.removeWatchedFolder(matched.path); + } } + await foldersApiService.deleteFolder(folder.id); + toast.success("Folder deleted"); + } catch (e: unknown) { + toast.error((e as Error)?.message || "Failed to delete folder"); } - await foldersApiService.deleteFolder(folder.id); - toast.success("Folder deleted"); - } catch (e: unknown) { - toast.error((e as Error)?.message || "Failed to delete folder"); - } - }, [electronAPI]); + }, + [electronAPI] + ); const handleMoveFolder = useCallback( (folder: FolderDisplay) => { diff --git a/surfsense_web/components/sources/DocumentUploadTab.tsx b/surfsense_web/components/sources/DocumentUploadTab.tsx index 28e160261..76af48c45 100644 --- a/surfsense_web/components/sources/DocumentUploadTab.tsx +++ b/surfsense_web/components/sources/DocumentUploadTab.tsx @@ -25,8 +25,8 @@ import { import { Progress } from "@/components/ui/progress"; import { Spinner } from "@/components/ui/spinner"; import { Switch } from "@/components/ui/switch"; -import { documentsApiService } from "@/lib/apis/documents-api.service"; import { useElectronAPI } from "@/hooks/use-platform"; +import { documentsApiService } from "@/lib/apis/documents-api.service"; import { trackDocumentUploadFailure, trackDocumentUploadStarted, diff --git a/surfsense_web/contexts/platform-context.tsx b/surfsense_web/contexts/platform-context.tsx index bb3e3800d..578901214 100644 --- a/surfsense_web/contexts/platform-context.tsx +++ b/surfsense_web/contexts/platform-context.tsx @@ -1,6 +1,6 @@ "use client"; -import { createContext, useEffect, useState, type ReactNode } from "react"; +import { createContext, type ReactNode, useEffect, useState } from "react"; export interface PlatformContextValue { isDesktop: boolean; @@ -25,7 +25,5 @@ export function PlatformProvider({ children }: { children: ReactNode }) { setValue({ isDesktop, isWeb: !isDesktop, electronAPI: api }); }, []); - return ( - {children} - ); + return {children}; } diff --git a/surfsense_web/types/window.d.ts b/surfsense_web/types/window.d.ts index 961ad9066..3f228066a 100644 --- a/surfsense_web/types/window.d.ts +++ b/surfsense_web/types/window.d.ts @@ -90,7 +90,9 @@ interface ElectronAPI { setAuthTokens: (bearer: string, refresh: string) => Promise; // Keyboard shortcut configuration getShortcuts: () => Promise<{ quickAsk: string; autocomplete: string }>; - setShortcuts: (config: Partial<{ quickAsk: string; autocomplete: string }>) => Promise<{ quickAsk: string; autocomplete: string }>; + setShortcuts: ( + config: Partial<{ quickAsk: string; autocomplete: string }> + ) => Promise<{ quickAsk: string; autocomplete: string }>; } declare global {