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}

}
{history.length === 0 ? ( diff --git a/src/renderer/pages/Settings.tsx b/src/renderer/pages/Settings.tsx index 8f5d381..fcc4028 100644 --- a/src/renderer/pages/Settings.tsx +++ b/src/renderer/pages/Settings.tsx @@ -92,6 +92,14 @@ export default function Settings({ } }, [isRecordingShortcut, handleKeyDown]); + // Cleanup timeout for API key success message + useEffect(() => { + if (apiKeySuccess) { + const timeoutId = setTimeout(() => setApiKeySuccess(false), 3000); + return () => clearTimeout(timeoutId); + } + }, [apiKeySuccess]); + const handleTransparencyChange = (value: boolean) => { onTransparencyChange(value); window.electronAPI.setTransparency(value); @@ -141,7 +149,6 @@ export default function Settings({ if (result.success) { setIsEditingApiKey(false); setApiKeySuccess(true); - setTimeout(() => setApiKeySuccess(false), 3000); } else { setApiKeyError(result.error || "Failed to save API key"); } From 2470f36a5db0ad14677ae787293b28049a63e592 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 12 Dec 2025 11:34:41 +0000 Subject: [PATCH 3/4] fix: resolve race condition in embedding generation Co-authored-by: mberrishdev <84536014+mberrishdev@users.noreply.github.com> --- src/main/clipboardManager.ts | 54 +++++++++++++++++++----------------- 1 file changed, 28 insertions(+), 26 deletions(-) diff --git a/src/main/clipboardManager.ts b/src/main/clipboardManager.ts index 5f6c888..1940389 100644 --- a/src/main/clipboardManager.ts +++ b/src/main/clipboardManager.ts @@ -110,32 +110,6 @@ export class ClipboardManager { embedding: [], }; - // 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); @@ -145,6 +119,34 @@ export class ClipboardManager { if (this.window) { this.window.webContents.send("clipboard-update", item); } + + // Generate embedding asynchronously without blocking clipboard monitoring + // Store the id in a variable to avoid race conditions + const itemId = id; + 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(itemId, 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 + }); } catch (error) { log.error("Failed to save text to database:", error); // Still add to memory even if DB fails From 08b1a63ab7f6f19a85ac921b6a62b1a30baadbc6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 12 Dec 2025 11:35:23 +0000 Subject: [PATCH 4/4] refactor: clean up unnecessary variable assignment Co-authored-by: mberrishdev <84536014+mberrishdev@users.noreply.github.com> --- src/main/clipboardManager.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/main/clipboardManager.ts b/src/main/clipboardManager.ts index 1940389..40939b8 100644 --- a/src/main/clipboardManager.ts +++ b/src/main/clipboardManager.ts @@ -121,8 +121,6 @@ export class ClipboardManager { } // Generate embedding asynchronously without blocking clipboard monitoring - // Store the id in a variable to avoid race conditions - const itemId = id; this.embeddingService .getEmbedding(trimmedText) .then((embedding) => { @@ -132,10 +130,9 @@ export class ClipboardManager { Array.isArray(embedding) && embedding.length > 0 ) { - item.embedding = embedding; // Update in database with embedding try { - this.db.updateItemEmbedding(itemId, embedding); + this.db.updateItemEmbedding(id, embedding); } catch (error) { log.error("Failed to update embedding in database:", error); }