From 556c54f6c1e71fae5e7e593b61f676aa433dd880 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 12 Dec 2025 11:27:35 +0000 Subject: [PATCH 1/4] Initial plan From 3ed5aef04f0bb9a278648e7205750526c8ea64f6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 12 Dec 2025 11:32:28 +0000 Subject: [PATCH 2/4] fix: address PR review comments for async operations and error handling Co-authored-by: mberrishdev <84536014+mberrishdev@users.noreply.github.com> --- src/main/clipboardManager.ts | 34 ++++++++++++++++++---- src/main/database.ts | 14 ++++++++- src/renderer/pages/ClipboardHistory.css | 12 ++++++++ src/renderer/pages/ClipboardHistory.tsx | 38 ++++++++++++++++++------- src/renderer/pages/Settings.tsx | 9 +++++- 5 files changed, 88 insertions(+), 19 deletions(-) diff --git a/src/main/clipboardManager.ts b/src/main/clipboardManager.ts index 4a8bf77..5f6c888 100644 --- a/src/main/clipboardManager.ts +++ b/src/main/clipboardManager.ts @@ -103,18 +103,40 @@ export class ClipboardManager { if (trimmedText && text !== this.lastClipboardText) { this.lastClipboardText = text; - const embedding = await this.embeddingService.getEmbedding( - trimmedText - ); - const item: ClipboardItem = { type: "text", text, timestamp: Date.now(), - embedding, + embedding: [], }; - // Save to database + // Generate embedding asynchronously without blocking clipboard monitoring + this.embeddingService + .getEmbedding(trimmedText) + .then((embedding) => { + // Validate embedding before assigning + if ( + embedding && + Array.isArray(embedding) && + embedding.length > 0 + ) { + item.embedding = embedding; + // Update in database with embedding + try { + this.db.updateItemEmbedding(item.id!, embedding); + } catch (error) { + log.error("Failed to update embedding in database:", error); + } + } else { + log.warn("Invalid or empty embedding received, skipping"); + } + }) + .catch((error) => { + log.error("Failed to generate embedding:", error); + // Continue without embedding - item is already saved + }); + + // Save to database immediately (without embedding) try { const id = this.db.addItem(item); item.id = id; diff --git a/src/main/database.ts b/src/main/database.ts index 9e026ab..97c973d 100644 --- a/src/main/database.ts +++ b/src/main/database.ts @@ -138,6 +138,17 @@ export class DatabaseManager { return stmt.all(`%${query}%`, limit) as ClipboardItem[]; } + updateItemEmbedding(id: number, embedding: number[]): void { + const stmt = this.db.prepare(` + UPDATE clipboard_items + SET embedding = vec_f32(?) + WHERE id = ? + `); + + const embeddingBlob = Buffer.from(new Float32Array(embedding).buffer); + stmt.run(embeddingBlob, id); + } + semanticSearch( queryEmbedding: number[], limit: number = 10 @@ -156,7 +167,8 @@ export class DatabaseManager { LIMIT ? `); - return stmt.all(JSON.stringify(queryEmbedding), limit) as ClipboardItem[]; + const embeddingBlob = Buffer.from(new Float32Array(queryEmbedding).buffer); + return stmt.all(embeddingBlob, limit) as ClipboardItem[]; } close() { diff --git a/src/renderer/pages/ClipboardHistory.css b/src/renderer/pages/ClipboardHistory.css index ed73416..6dd0341 100644 --- a/src/renderer/pages/ClipboardHistory.css +++ b/src/renderer/pages/ClipboardHistory.css @@ -162,6 +162,18 @@ body.opaque .app { letter-spacing: 0.03em; } +.search-error { + text-align: center; + font-size: 0.75rem; + color: #ef4444; + margin-top: 0.5rem; + padding: 0.5rem 1rem; + background: rgba(239, 68, 68, 0.1); + border: 1px solid rgba(239, 68, 68, 0.2); + border-radius: 8px; + font-weight: 500; +} + .search-icon { position: absolute; left: 1rem; diff --git a/src/renderer/pages/ClipboardHistory.tsx b/src/renderer/pages/ClipboardHistory.tsx index ac8a2cf..d0e276e 100644 --- a/src/renderer/pages/ClipboardHistory.tsx +++ b/src/renderer/pages/ClipboardHistory.tsx @@ -2,6 +2,7 @@ import { useState, useEffect } from "react"; import type { ClipboardItem as ClipboardItemType } from "../../models/ClipboardItem"; import HistoryItemCard from "../components/ClipboardItem"; import "./ClipboardHistory.css"; +import log from "electron-log/renderer"; interface ClipboardHistoryProps {} @@ -11,29 +12,40 @@ export default function ClipboardHistory({}: ClipboardHistoryProps) { const [hasMore, setHasMore] = useState(true); const [searchQuery, setSearchQuery] = useState(""); const [hasApiKey, setHasApiKey] = useState(false); + const [searchError, setSearchError] = useState(""); const performSearch = async () => { - console.log("Search triggered for:", searchQuery); + log.info("Search triggered for:", searchQuery); if (searchQuery.trim()) { + if (!hasApiKey) { + setSearchError("Please configure your OpenAI API key in Settings to use semantic search."); + return; + } + setIsLoading(true); + setSearchError(""); try { - console.log("Performing semantic search for:", searchQuery); + log.info("Performing semantic search for:", searchQuery); const results = await window.electronAPI.semanticSearch( searchQuery, 10 ); - console.log(`Found ${results.length} results:`, results); + log.info(`Found ${results.length} results:`, results); setHistory(results); + setHasMore(false); // Disable load more for search results } catch (error) { - console.error("Semantic search failed:", error); + log.error("Semantic search failed:", error); + setSearchError("Search failed. Please check your API key and try again."); } finally { setIsLoading(false); } } else { - console.log("Empty query, reloading full history"); + log.info("Empty query, reloading full history"); + setSearchError(""); const fullHistory = await window.electronAPI.getClipboardHistory(); setHistory(fullHistory); + setHasMore(true); } }; @@ -45,7 +57,7 @@ export default function ClipboardHistory({}: ClipboardHistoryProps) { useEffect(() => { window.electronAPI.getClipboardHistory().then((items) => { - console.log(`Initial history loaded: ${items.length} items`); + log.info(`Initial history loaded: ${items.length} items`); setHistory(items); }); @@ -55,29 +67,30 @@ export default function ClipboardHistory({}: ClipboardHistoryProps) { window.electronAPI.onClipboardUpdate((item) => { setHistory((prev) => [item, ...prev]); + setHasMore(true); }); }, []); const loadMore = async () => { if (isLoading || !hasMore) { - console.log( + log.info( `loadMore blocked: isLoading=${isLoading}, hasMore=${hasMore}` ); return; } - console.log("loadMore: Fetching more items..."); + log.info("loadMore: Fetching more items..."); setIsLoading(true); try { const moreItems = await window.electronAPI.loadMoreHistory(20); - console.log(`Loaded ${moreItems.length} more clipboard items`); + log.info(`Loaded ${moreItems.length} more clipboard items`); if (moreItems.length === 0) { setHasMore(false); } else { setHistory((prev) => [...prev, ...moreItems]); } } catch (error) { - console.error("Failed to load more items:", error); + log.error("Failed to load more items:", error); } finally { setIsLoading(false); } @@ -137,10 +150,12 @@ export default function ClipboardHistory({}: ClipboardHistoryProps) { className="clear-btn" onClick={async () => { setSearchQuery(""); - console.log("Search cleared, reloading full history"); + setSearchError(""); + log.info("Search cleared, reloading full history"); const fullHistory = await window.electronAPI.getClipboardHistory(); setHistory(fullHistory); + setHasMore(true); }} aria-label="Clear search" > @@ -163,6 +178,7 @@ export default function ClipboardHistory({}: ClipboardHistoryProps) { search.
)} + {searchError &&{searchError}
}