Skip to content

Commit dd710da

Browse files
committed
release: 0.5.3
1 parent adcfb14 commit dd710da

14 files changed

Lines changed: 647 additions & 56 deletions

AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ swift test
7878
- Fuzzy search must be instant.
7979
- QMD keyword + semantic results augment fuzzy.
8080
- QMD keyword threshold: 3+ chars.
81-
- QMD semantic threshold: 3+ chars.
81+
- QMD semantic threshold: 5+ chars.
8282

8383
5. **Layout stability**
8484
- Avoid row height jitter / implicit list animation regressions.

CHANGELOG.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@ All notable changes to this project are documented in this file.
44

55
## [Unreleased]
66

7+
## [0.5.3] - 2026-04-09
8+
9+
### Fixed
10+
11+
- Fixed paste-back targeting so `Return` waits for the previously focused app to reactivate before sending `⌘V` instead of firing into nowhere.
12+
- Preserved real Finder/file-url clipboard payloads for path entries, so copied files paste back as files instead of plain-text paths.
13+
- Re-expanded the visible row window around restored selections so delete/undo keeps focus on the restored row even in large filtered result sets.
14+
715
### Performance
816

917
- Moved clipboard filtering/ranking off the main actor so large histories and long clipboard items no longer block typing or overlay refresh while search results are recomputed.
@@ -14,7 +22,7 @@ All notable changes to this project are documented in this file.
1422
- Moved overlay preview details and image thumbnails off the SwiftUI render path so search, delete, selection, and preview interactions no longer decode images or scan large text synchronously on the main thread.
1523
- Suspended clipboard polling while the overlay is visible, reduced delete-cleanup churn, and stopped delete/undo from clearing live QMD results just to rerun the same query.
1624
- Replaced full-file image duplicate reads with sampled image signatures and switched image paste-back/preview loading away from synchronous full-image decodes.
17-
- Disabled QMD runtime search/index integration so overlay interactions now stay on the local fuzzy-search path and no longer spawn qmd work while the app is running.
25+
- Restored QMD runtime search/index integration behind the async fuzzy-first pipeline so overlay typing stays responsive while keyword/semantic results merge in later.
1826
- Made local search/regroup work cooperatively cancellable, raised search-input coalescing, and lowered overlay rebuild priority so quick search edits stop piling up stale rescans.
1927
- Reduced overlay layout churn by removing animated auto-scroll on search-driven selection changes, trimming inline preview text work, and rendering the overlay list through a rolling 100-entry window that expands as you scroll.
2028
- Replaced the custom overlay `ScrollView`/`LazyVStack` history pane with a native `List` while keeping search scoped to the full result set, which fixes the remaining fast-scroll lag without narrowing search coverage.

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,8 +127,8 @@ You can also right-click the app and choose **Open** once to allow launch.
127127

128128
## Search behavior
129129

130-
Search currently uses only the local fuzzy ranking pipeline.
131-
The previous QMD augmentation path has been disabled to prioritize instant overlay responsiveness on large histories.
130+
Search uses an instant local fuzzy ranking pipeline first, then asynchronously appends QMD results without blocking typing or list refresh.
131+
QMD keyword search starts at 3+ characters, and semantic search starts at 5+ characters.
132132
To keep the overlay responsive, the live list starts with 100 rendered matches and expands the window as you scroll while still searching across the full result set.
133133

134134
## Troubleshooting

Sources/CBManager/ClipboardDatabase.swift

Lines changed: 47 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ final class ClipboardDatabase {
2424

2525
func loadEntries() -> [ClipboardEntry] {
2626
let sql = """
27-
SELECT id, created_at, source_app, kind, content, image_path, ocr_text, ocr_pending,
27+
SELECT id, created_at, source_app, kind, content, file_urls, image_path, ocr_text, ocr_pending,
2828
ai_title, ai_title_pending
2929
FROM clipboard_entries
3030
ORDER BY created_at DESC;
@@ -40,11 +40,12 @@ final class ClipboardDatabase {
4040
let sourceApp = string(at: 2, from: statement)
4141
let kindString = string(at: 3, from: statement) ?? ClipboardEntry.Kind.text.rawValue
4242
let content = string(at: 4, from: statement) ?? ""
43-
let imagePath = string(at: 5, from: statement)
44-
let ocrText = string(at: 6, from: statement) ?? ""
45-
let isPending = sqlite3_column_int(statement, 7) != 0
46-
let aiTitle = string(at: 8, from: statement) ?? ""
47-
let aiTitlePending = sqlite3_column_int(statement, 9) != 0
43+
let fileURLs = decodeFileURLs(from: string(at: 5, from: statement))
44+
let imagePath = string(at: 6, from: statement)
45+
let ocrText = string(at: 7, from: statement) ?? ""
46+
let isPending = sqlite3_column_int(statement, 8) != 0
47+
let aiTitle = string(at: 9, from: statement) ?? ""
48+
let aiTitlePending = sqlite3_column_int(statement, 10) != 0
4849

4950
let kind = ClipboardEntry.Kind(rawValue: kindString) ?? .text
5051
result.append(
@@ -54,6 +55,7 @@ final class ClipboardDatabase {
5455
date: Date(timeIntervalSince1970: createdAt),
5556
sourceApp: sourceApp,
5657
kind: kind,
58+
fileURLs: fileURLs,
5759
imagePath: imagePath,
5860
ocrText: ocrText,
5961
isOCRPending: isPending,
@@ -69,8 +71,8 @@ final class ClipboardDatabase {
6971
func insert(_ entry: ClipboardEntry) {
7072
let sql = """
7173
INSERT OR REPLACE INTO clipboard_entries
72-
(id, created_at, source_app, kind, content, image_path, ocr_text, ocr_pending, ai_title, ai_title_pending)
73-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
74+
(id, created_at, source_app, kind, content, file_urls, image_path, ocr_text, ocr_pending, ai_title, ai_title_pending)
75+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
7476
"""
7577

7678
guard let statement = prepare(sql) else { return }
@@ -81,11 +83,12 @@ final class ClipboardDatabase {
8183
sqlite3_bind_nullable_text(statement, 3, entry.sourceApp)
8284
sqlite3_bind_text(statement, 4, entry.kind.rawValue, -1, SQLITE_TRANSIENT)
8385
sqlite3_bind_text(statement, 5, entry.content, -1, SQLITE_TRANSIENT)
84-
sqlite3_bind_nullable_text(statement, 6, entry.imagePath)
85-
sqlite3_bind_text(statement, 7, entry.ocrText, -1, SQLITE_TRANSIENT)
86-
sqlite3_bind_int(statement, 8, entry.isOCRPending ? 1 : 0)
87-
sqlite3_bind_text(statement, 9, entry.aiTitle, -1, SQLITE_TRANSIENT)
88-
sqlite3_bind_int(statement, 10, entry.isAITitlePending ? 1 : 0)
86+
sqlite3_bind_nullable_text(statement, 6, encodeFileURLs(entry.fileURLs))
87+
sqlite3_bind_nullable_text(statement, 7, entry.imagePath)
88+
sqlite3_bind_text(statement, 8, entry.ocrText, -1, SQLITE_TRANSIENT)
89+
sqlite3_bind_int(statement, 9, entry.isOCRPending ? 1 : 0)
90+
sqlite3_bind_text(statement, 10, entry.aiTitle, -1, SQLITE_TRANSIENT)
91+
sqlite3_bind_int(statement, 11, entry.isAITitlePending ? 1 : 0)
8992

9093
sqlite3_step(statement)
9194
}
@@ -127,7 +130,7 @@ final class ClipboardDatabase {
127130

128131
// First, load the entries that will be deleted so callers can clean up image files.
129132
let selectSQL = """
130-
SELECT id, created_at, source_app, kind, content, image_path, ocr_text, ocr_pending,
133+
SELECT id, created_at, source_app, kind, content, file_urls, image_path, ocr_text, ocr_pending,
131134
ai_title, ai_title_pending
132135
FROM clipboard_entries
133136
WHERE created_at < ?
@@ -144,11 +147,12 @@ final class ClipboardDatabase {
144147
let sourceApp = string(at: 2, from: selectStmt)
145148
let kindString = string(at: 3, from: selectStmt) ?? ClipboardEntry.Kind.text.rawValue
146149
let content = string(at: 4, from: selectStmt) ?? ""
147-
let imagePath = string(at: 5, from: selectStmt)
148-
let ocrText = string(at: 6, from: selectStmt) ?? ""
149-
let isPending = sqlite3_column_int(selectStmt, 7) != 0
150-
let aiTitle = string(at: 8, from: selectStmt) ?? ""
151-
let aiTitlePending = sqlite3_column_int(selectStmt, 9) != 0
150+
let fileURLs = decodeFileURLs(from: string(at: 5, from: selectStmt))
151+
let imagePath = string(at: 6, from: selectStmt)
152+
let ocrText = string(at: 7, from: selectStmt) ?? ""
153+
let isPending = sqlite3_column_int(selectStmt, 8) != 0
154+
let aiTitle = string(at: 9, from: selectStmt) ?? ""
155+
let aiTitlePending = sqlite3_column_int(selectStmt, 10) != 0
152156

153157
let kind = ClipboardEntry.Kind(rawValue: kindString) ?? .text
154158
removed.append(ClipboardEntry(
@@ -157,6 +161,7 @@ final class ClipboardDatabase {
157161
date: Date(timeIntervalSince1970: createdAt),
158162
sourceApp: sourceApp,
159163
kind: kind,
164+
fileURLs: fileURLs,
160165
imagePath: imagePath,
161166
ocrText: ocrText,
162167
isOCRPending: isPending,
@@ -193,9 +198,12 @@ final class ClipboardDatabase {
193198
source_app TEXT,
194199
kind TEXT NOT NULL,
195200
content TEXT,
201+
file_urls TEXT,
196202
image_path TEXT,
197203
ocr_text TEXT,
198-
ocr_pending INTEGER NOT NULL DEFAULT 0
204+
ocr_pending INTEGER NOT NULL DEFAULT 0,
205+
ai_title TEXT NOT NULL DEFAULT '',
206+
ai_title_pending INTEGER NOT NULL DEFAULT 0
199207
);
200208
201209
CREATE INDEX IF NOT EXISTS idx_clipboard_entries_created_at ON clipboard_entries(created_at DESC);
@@ -206,6 +214,7 @@ final class ClipboardDatabase {
206214

207215
private func migrateIfNeeded() {
208216
_ = sqlite3_exec(db, "ALTER TABLE clipboard_entries ADD COLUMN ocr_pending INTEGER NOT NULL DEFAULT 0;", nil, nil, nil)
217+
_ = sqlite3_exec(db, "ALTER TABLE clipboard_entries ADD COLUMN file_urls TEXT;", nil, nil, nil)
209218
_ = sqlite3_exec(db, "ALTER TABLE clipboard_entries ADD COLUMN ai_title TEXT NOT NULL DEFAULT '';", nil, nil, nil)
210219
_ = sqlite3_exec(db, "ALTER TABLE clipboard_entries ADD COLUMN ai_title_pending INTEGER NOT NULL DEFAULT 0;", nil, nil, nil)
211220
}
@@ -223,6 +232,24 @@ final class ClipboardDatabase {
223232
guard let cString = sqlite3_column_text(statement, index) else { return nil }
224233
return String(cString: cString)
225234
}
235+
236+
private func encodeFileURLs(_ urls: [String]) -> String? {
237+
guard !urls.isEmpty,
238+
let data = try? JSONEncoder().encode(urls),
239+
let encoded = String(data: data, encoding: .utf8) else {
240+
return nil
241+
}
242+
return encoded
243+
}
244+
245+
private func decodeFileURLs(from value: String?) -> [String] {
246+
guard let value,
247+
let data = value.data(using: .utf8),
248+
let urls = try? JSONDecoder().decode([String].self, from: data) else {
249+
return []
250+
}
251+
return urls
252+
}
226253
}
227254

228255
private extension OpaquePointer {

0 commit comments

Comments
 (0)