chore: linting

This commit is contained in:
DESKTOP-RTLN3BA\$punk 2026-04-07 03:10:06 -07:00
parent 82b5c7f19e
commit 91ea293fa2
14 changed files with 285 additions and 264 deletions

View file

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

View file

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

View file

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

View file

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

View file

@ -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() {
<div
className="absolute -top-1/2 left-1/2 size-[800px] -translate-x-1/2 rounded-full opacity-[0.03]"
style={{
background:
"radial-gradient(circle, hsl(var(--primary)) 0%, transparent 70%)",
background: "radial-gradient(circle, hsl(var(--primary)) 0%, transparent 70%)",
}}
/>
</div>
@ -135,9 +121,7 @@ export default function DesktopLoginPage() {
priority
/>
<CardTitle className="text-xl">Welcome to SurfSense Desktop App</CardTitle>
<CardDescription>
Configure your shortcuts, then sign in to get started.
</CardDescription>
<CardDescription>Configure your shortcuts, then sign in to get started.</CardDescription>
</CardHeader>
<CardContent className="flex flex-col gap-6">
@ -181,11 +165,7 @@ export default function DesktopLoginPage() {
{/* ---- Auth Section (second) ---- */}
{isGoogleAuth ? (
<Button
variant="outline"
className="w-full gap-2 py-5"
onClick={handleGoogleLogin}
>
<Button variant="outline" className="w-full gap-2 py-5" onClick={handleGoogleLogin}>
<IconBrandGoogleFilled className="size-5" />
Continue with Google
</Button>
@ -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 ? (
<EyeOff className="size-4" />
) : (
<Eye className="size-4" />
)}
{showPassword ? <EyeOff className="size-4" /> : <Eye className="size-4" />}
</button>
</div>
</div>

View file

@ -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
</Button>
<button
type="button"
onClick={handleSkip}
className="block mx-auto text-xs text-muted-foreground hover:text-foreground transition-colors"
>

View file

@ -2,7 +2,7 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { useElectronAPI } from "@/hooks/use-platform";
import { getBearerToken, ensureTokensFromElectron } from "@/lib/auth-utils";
import { ensureTokensFromElectron, getBearerToken } from "@/lib/auth-utils";
type SSEEvent =
| { type: "text-delta"; id: string; delta: string }
@ -48,9 +48,20 @@ const AUTO_DISMISS_MS = 3000;
function StepIcon({ status }: { status: string }) {
if (status === "complete") {
return (
<svg className="step-icon step-icon-done" viewBox="0 0 16 16" fill="none">
<svg
className="step-icon step-icon-done"
viewBox="0 0 16 16"
fill="none"
aria-label="Step complete"
>
<circle cx="8" cy="8" r="7" stroke="#4ade80" strokeWidth="1.5" />
<path d="M5 8.5l2 2 4-4.5" stroke="#4ade80" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path
d="M5 8.5l2 2 4-4.5"
stroke="#4ade80"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}

View file

@ -19,59 +19,59 @@ body:has(.suggestion-body) {
}
.suggestion-tooltip {
box-sizing: border-box;
background: #1e1e1e;
border: 1px solid #3c3c3c;
border-radius: 8px;
padding: 8px 12px;
margin: 4px;
max-width: 400px;
/* MAX_HEIGHT in suggestion-window.ts is 400px. Subtract 8px for margin
box-sizing: border-box;
background: #1e1e1e;
border: 1px solid #3c3c3c;
border-radius: 8px;
padding: 8px 12px;
margin: 4px;
max-width: 400px;
/* MAX_HEIGHT in suggestion-window.ts is 400px. Subtract 8px for margin
(4px * 2) so the tooltip + margin fits within the Electron window.
box-sizing: border-box ensures padding + border are included. */
max-height: 392px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.5);
display: flex;
flex-direction: column;
overflow: hidden;
max-height: 392px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.5);
display: flex;
flex-direction: column;
overflow: hidden;
}
.suggestion-text {
color: #d4d4d4;
font-size: 13px;
line-height: 1.45;
margin: 0 0 6px 0;
word-wrap: break-word;
white-space: pre-wrap;
overflow-y: auto;
flex: 1 1 auto;
min-height: 0;
color: #d4d4d4;
font-size: 13px;
line-height: 1.45;
margin: 0 0 6px 0;
word-wrap: break-word;
white-space: pre-wrap;
overflow-y: auto;
flex: 1 1 auto;
min-height: 0;
}
.suggestion-text::-webkit-scrollbar {
width: 5px;
width: 5px;
}
.suggestion-text::-webkit-scrollbar-track {
background: transparent;
background: transparent;
}
.suggestion-text::-webkit-scrollbar-thumb {
background: #555;
border-radius: 3px;
background: #555;
border-radius: 3px;
}
.suggestion-text::-webkit-scrollbar-thumb:hover {
background: #777;
background: #777;
}
.suggestion-actions {
display: flex;
justify-content: flex-end;
gap: 4px;
border-top: 1px solid #2a2a2a;
padding-top: 6px;
flex-shrink: 0;
display: flex;
justify-content: flex-end;
gap: 4px;
border-top: 1px solid #2a2a2a;
padding-top: 6px;
flex-shrink: 0;
}
.suggestion-btn {
@ -120,74 +120,74 @@ body:has(.suggestion-body) {
/* --- Agent activity indicator --- */
.agent-activity {
display: flex;
flex-direction: column;
gap: 4px;
overflow-y: auto;
max-height: 340px;
display: flex;
flex-direction: column;
gap: 4px;
overflow-y: auto;
max-height: 340px;
}
.activity-initial {
display: flex;
align-items: center;
gap: 8px;
padding: 2px 0;
display: flex;
align-items: center;
gap: 8px;
padding: 2px 0;
}
.activity-label {
color: #a1a1aa;
font-size: 12px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
color: #a1a1aa;
font-size: 12px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.activity-steps {
display: flex;
flex-direction: column;
gap: 3px;
display: flex;
flex-direction: column;
gap: 3px;
}
.activity-step {
display: flex;
align-items: center;
gap: 6px;
min-height: 18px;
display: flex;
align-items: center;
gap: 6px;
min-height: 18px;
}
.step-label {
color: #d4d4d4;
font-size: 12px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
color: #d4d4d4;
font-size: 12px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.step-detail {
color: #71717a;
font-size: 11px;
color: #71717a;
font-size: 11px;
}
/* Spinner (in_progress) */
.step-spinner {
width: 14px;
height: 14px;
flex-shrink: 0;
border: 1.5px solid #3f3f46;
border-top-color: #a78bfa;
border-radius: 50%;
animation: step-spin 0.7s linear infinite;
width: 14px;
height: 14px;
flex-shrink: 0;
border: 1.5px solid #3f3f46;
border-top-color: #a78bfa;
border-radius: 50%;
animation: step-spin 0.7s linear infinite;
}
/* Checkmark icon (complete) */
.step-icon {
width: 14px;
height: 14px;
flex-shrink: 0;
width: 14px;
height: 14px;
flex-shrink: 0;
}
@keyframes step-spin {
to {
transform: rotate(360deg);
}
to {
transform: rotate(360deg);
}
}

View file

@ -44,13 +44,7 @@ export const DEFAULT_SHORTCUTS = {
// Kbd pill component
// ---------------------------------------------------------------------------
export function Kbd({
keys,
className,
}: {
keys: string[];
className?: string;
}) {
export function Kbd({ keys, className }: { keys: string[]; className?: string }) {
return (
<span className={cn("inline-flex items-center gap-1", className)}>
{keys.map((key) => (
@ -123,9 +117,7 @@ export function ShortcutRecorder({
</div>
<div className="min-w-0">
<p className="text-sm font-medium leading-none">{label}</p>
<p className="mt-1 text-xs text-muted-foreground truncate">
{description}
</p>
<p className="mt-1 text-xs text-muted-foreground truncate">{description}</p>
</div>
</div>
@ -155,9 +147,7 @@ export function ShortcutRecorder({
)}
>
{recording ? (
<span className="text-xs text-primary animate-pulse">
Press keys...
</span>
<span className="text-xs text-primary animate-pulse">Press keys...</span>
) : (
<Kbd keys={displayKeys} />
)}

View file

@ -1,21 +1,14 @@
"use client";
import { AnimatePresence, motion } from "motion/react";
import { Monitor } from "lucide-react";
import { AnimatePresence, motion } from "motion/react";
import Link from "next/link";
import React, { useCallback, useEffect, useRef, useState, memo } from "react";
import React, { memo, useCallback, useEffect, useRef, useState } from "react";
import Balancer from "react-wrap-balancer";
import { ExpandedMediaOverlay, useExpandedMedia } from "@/components/ui/expanded-gif-overlay";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { AUTH_TYPE, BACKEND_URL } from "@/lib/env-config";
import { trackLoginAttempt } from "@/lib/posthog/events";
import { cn } from "@/lib/utils";
import {
ExpandedMediaOverlay,
useExpandedMedia,
} from "@/components/ui/expanded-gif-overlay";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
const GoogleLogo = ({ className }: { className?: string }) => (
<svg
@ -61,8 +54,7 @@ const TAB_ITEMS = [
},
{
title: "Search & Citation",
description:
"Ask questions and get cited responses from your knowledge base.",
description: "Ask questions and get cited responses from your knowledge base.",
src: "/homepage/hero_tutorial/BSNCGif.mp4",
featured: false,
},
@ -86,15 +78,13 @@ const TAB_ITEMS = [
},
{
title: "Image Generation",
description:
"Generate high-quality images easily from your conversations.",
description: "Generate high-quality images easily from your conversations.",
src: "/homepage/hero_tutorial/ImageGenGif.mp4",
featured: false,
},
{
title: "Collaborative Chat",
description:
"Collaborate on AI-powered conversations in realtime with your team.",
description: "Collaborate on AI-powered conversations in realtime with your team.",
src: "/homepage/hero_realtime/RealTimeChatGif.mp4",
featured: false,
},
@ -106,8 +96,7 @@ const TAB_ITEMS = [
},
{
title: "Video Generation",
description:
"Create short videos with AI-generated visuals and narration from your sources.",
description: "Create short videos with AI-generated visuals and narration from your sources.",
src: "/homepage/hero_tutorial/video_gen_surf.mp4",
featured: false,
},
@ -119,7 +108,7 @@ export function HeroSection() {
<div className="mt-4 flex w-full min-w-0 flex-col items-start px-2 md:px-8 xl:px-0">
<h1
className={cn(
"relative mt-4 max-w-7xl text-left text-4xl font-bold tracking-tight text-balance text-neutral-900 sm:text-5xl md:text-6xl xl:text-8xl dark:text-neutral-50",
"relative mt-4 max-w-7xl text-left text-4xl font-bold tracking-tight text-balance text-neutral-900 sm:text-5xl md:text-6xl xl:text-8xl dark:text-neutral-50"
)}
>
<Balancer>NotebookLM for Teams</Balancer>
@ -128,10 +117,11 @@ export function HeroSection() {
<div>
<h2
className={cn(
"relative mb-8 max-w-2xl text-left text-sm tracking-wide text-neutral-600 antialiased sm:text-base md:text-xl dark:text-neutral-400",
"relative mb-8 max-w-2xl text-left text-sm tracking-wide text-neutral-600 antialiased sm:text-base md:text-xl dark:text-neutral-400"
)}
>
An open source, privacy focused alternative to NotebookLM for teams with no data limits.
An open source, privacy focused alternative to NotebookLM for teams with no data
limits.
</h2>
<div className="relative mb-4 flex w-full flex-col justify-center gap-y-2 sm:flex-row sm:justify-start sm:space-y-0 sm:space-x-4">
@ -194,33 +184,34 @@ const BrowserWindow = () => {
<div className="no-visible-scrollbar flex min-w-0 shrink flex-row items-center justify-start gap-2 overflow-x-auto mask-l-from-98% py-0.5 pr-2 pl-2 md:pl-4">
{TAB_ITEMS.map((item, index) => (
<React.Fragment key={item.title}>
<button
type="button"
onClick={() => setSelectedIndex(index)}
className={cn(
"flex shrink-0 items-center gap-1.5 rounded-md px-2 py-1 text-xs transition duration-150 hover:bg-white sm:text-sm dark:hover:bg-neutral-950",
selectedIndex === index && !item.featured &&
"bg-white shadow ring-1 shadow-black/10 ring-black/10 dark:bg-neutral-900",
selectedIndex === index && item.featured &&
"bg-amber-50 shadow ring-1 shadow-amber-200/50 ring-amber-400/60 dark:bg-amber-950/40 dark:shadow-amber-900/30 dark:ring-amber-500/50",
item.featured && selectedIndex !== index &&
"hover:bg-amber-50 dark:hover:bg-amber-950/30",
)}
>
<button
type="button"
onClick={() => setSelectedIndex(index)}
className={cn(
"flex shrink-0 items-center gap-1.5 rounded-md px-2 py-1 text-xs transition duration-150 hover:bg-white sm:text-sm dark:hover:bg-neutral-950",
selectedIndex === index &&
!item.featured &&
"bg-white shadow ring-1 shadow-black/10 ring-black/10 dark:bg-neutral-900",
selectedIndex === index &&
item.featured &&
"bg-amber-50 shadow ring-1 shadow-amber-200/50 ring-amber-400/60 dark:bg-amber-950/40 dark:shadow-amber-900/30 dark:ring-amber-500/50",
item.featured &&
selectedIndex !== index &&
"hover:bg-amber-50 dark:hover:bg-amber-950/30"
)}
>
{item.title}
{item.featured && (
<Tooltip>
<TooltipTrigger asChild>
<span className="inline-flex shrink-0 items-center justify-center rounded border border-amber-300 bg-amber-100 p-0.5 text-amber-700 dark:border-amber-700 dark:bg-amber-900/50 dark:text-amber-400">
<Monitor className="size-3" />
</span>
</TooltipTrigger>
<TooltipContent side="bottom">
Desktop app only
</TooltipContent>
</Tooltip>
)}
</button>
{item.featured && (
<Tooltip>
<TooltipTrigger asChild>
<span className="inline-flex shrink-0 items-center justify-center rounded border border-amber-300 bg-amber-100 p-0.5 text-amber-700 dark:border-amber-700 dark:bg-amber-900/50 dark:text-amber-400">
<Monitor className="size-3" />
</span>
</TooltipTrigger>
<TooltipContent side="bottom">Desktop app only</TooltipContent>
</Tooltip>
)}
</button>
{index !== TAB_ITEMS.length - 1 && (
<div className="h-4 w-px shrink-0 rounded-full bg-neutral-300 dark:bg-neutral-700" />
)}
@ -263,13 +254,13 @@ const BrowserWindow = () => {
</p>
</div>
</div>
{/* biome-ignore lint/a11y/useKeyWithClickEvents: wrapper for video expand */}
<div
className="cursor-pointer bg-neutral-50 p-2 sm:p-3 dark:bg-neutral-950"
<button
type="button"
className="cursor-pointer bg-neutral-50 p-2 sm:p-3 dark:bg-neutral-950 w-full"
onClick={open}
>
<TabVideo src={selectedItem.src} />
</div>
</button>
</motion.div>
</AnimatePresence>
</div>
@ -277,11 +268,7 @@ const BrowserWindow = () => {
<AnimatePresence>
{expanded && (
<ExpandedMediaOverlay
src={selectedItem.src}
alt={selectedItem.title}
onClose={close}
/>
<ExpandedMediaOverlay src={selectedItem.src} alt={selectedItem.title} onClose={close} />
)}
</AnimatePresence>
</>
@ -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"
>
<svg className="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<svg
className="size-4"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
aria-label="Download for macOS"
>
<path d="M12 17V3" />
<path d="m6 11 6 6 6-6" />
<path d="M19 21H5" />
@ -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"
>
<svg className="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<svg
className="size-4"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
aria-label="Download for Windows"
>
<path d="M12 17V3" />
<path d="m6 11 6 6 6-6" />
<path d="M19 21H5" />
@ -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"
>
<svg className="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<svg
className="size-4"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
aria-label="Download for Linux"
>
<path d="M12 17V3" />
<path d="m6 11 6 6 6-6" />
<path d="M19 21H5" />

View file

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

View file

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

View file

@ -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 (
<PlatformContext.Provider value={value}>{children}</PlatformContext.Provider>
);
return <PlatformContext.Provider value={value}>{children}</PlatformContext.Provider>;
}

View file

@ -90,7 +90,9 @@ interface ElectronAPI {
setAuthTokens: (bearer: string, refresh: string) => Promise<void>;
// 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 {