Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
a5654f6
Add capture: share-from-anywhere, quick capture, voice notes, git hook
Vasilije1990 Aug 5, 2026
dfbc2ad
Merge chunk_text fix from base branch
Vasilije1990 Aug 5, 2026
434da49
Merge source-connection chips and mock connectors from base branch
Vasilije1990 Aug 5, 2026
e06c9a7
Fix hotkey crosstalk: dispatch Carbon events by hotkey ID
Vasilije1990 Aug 5, 2026
358ebfb
Merge self-describing sources from base branch
Vasilije1990 Aug 5, 2026
064163b
Merge hover-expanding connection chips from base branch
Vasilije1990 Aug 5, 2026
b84716e
Merge GitHub source with dataset-per-repo from base branch
Vasilije1990 Aug 5, 2026
0bb4bdb
Merge cloud-compatible latent features from base branch
Vasilije1990 Aug 5, 2026
055710d
Merge hint-bar contrast fix from base branch
Vasilije1990 Aug 5, 2026
c5217a6
Merge connection detail view from base branch
Vasilije1990 Aug 5, 2026
6a6f4b2
Merge folder-count fix from base branch
Vasilije1990 Aug 5, 2026
2314c90
Merge copy buttons and modern inbox from base branch
Vasilije1990 Aug 6, 2026
de851d7
Merge proactive assistant from base branch
Vasilije1990 Aug 6, 2026
a12da50
Quick capture whispers back: related memory and conflicts while typing
Vasilije1990 Aug 6, 2026
e805e44
Merge connection scope dropdown from base branch
Vasilije1990 Aug 6, 2026
0dd3752
Merge PR #330 review fixes from base branch
Vasilije1990 Aug 10, 2026
dd28fda
Merge product rename from base branch
Vasilije1990 Aug 10, 2026
463f34d
Merge profile-switch fixes from base branch
Vasilije1990 Aug 11, 2026
0e72065
Local mode ties the integrations together: full adapter parity
Vasilije1990 Aug 11, 2026
75ee313
Merge refusal-filter widening from base branch
Vasilije1990 Aug 13, 2026
0a0172b
Merge mobile connection and Android demo from base branch
Vasilije1990 Aug 13, 2026
fe9289c
Add the Android demo's gradle scaffolding and bundled answer
Vasilije1990 Aug 13, 2026
d80aaf2
Merge file-level indexing and index browser from base branch
Vasilije1990 Aug 18, 2026
213430b
Merge Android artifact cleanup from base branch
Vasilije1990 Aug 18, 2026
2ee4a77
Merge GitHub star nudge from base branch
Vasilije1990 Aug 18, 2026
af7add5
Merge Settings connections section from base branch
Vasilije1990 Aug 18, 2026
5460bd9
Merge index removal from base branch
Vasilije1990 Aug 18, 2026
0593ffe
Merge single-file forget fix from base branch
Vasilije1990 Aug 18, 2026
89a2221
Merge forget stem-match fix from base branch
Vasilije1990 Aug 18, 2026
72d510f
Merge extension filters, Finder indexing, setup rewrite from base
Vasilije1990 Aug 20, 2026
1fbedff
Merge tombstones, labels, stacked answer from base
Vasilije1990 Aug 20, 2026
cfe62d7
Merge connected-agents visibility from base
Vasilije1990 Aug 23, 2026
a7bb0d2
Merge plugin-identity agents view from base
Vasilije1990 Aug 23, 2026
2939597
Merge demo-labeled mocks from base
Vasilije1990 Aug 23, 2026
638b7c0
Merge dark-background material fix from base
Vasilije1990 Aug 31, 2026
d52e877
Capture bar matches the panel's regular material
Vasilije1990 Aug 31, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions integrations/desktop/desktop_backend/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@
".txt",
".rst",
".org",
# voice notes and meeting recordings — cognee's loaders transcribe audio
".mp3",
".m4a",
".wav",
".aac",
".pdf",
".docx",
".pptx",
Expand Down
33 changes: 33 additions & 0 deletions integrations/desktop/desktop_backend/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,12 @@ class FeedbackRequest(BaseModel):
rating: int # 1-5; >=4 reinforces memory


class CaptureRequest(BaseModel):
text: str
title: str = ""
source: str = "" # e.g. "quick-capture", "git:repo-name", "share-sheet"


class ForgetRequest(BaseModel):
path: str # an indexed file, or a whole watched root

Expand Down Expand Up @@ -476,6 +482,33 @@ def cache_put(key: tuple, value: dict) -> None:
search_cache.pop(next(iter(search_cache)))
search_cache[key] = (time.time(), value)

@app.post("/capture", status_code=202)
async def capture(request: CaptureRequest) -> dict:
"""One thought (or commit, or shared selection) into memory.

The note lands in the capture folder and indexes like any document —
quick-capture hotkey, the share CLI, and the git hook all funnel here.
"""
import re as _re
import time as _time

text = request.text.strip()
if not text:
return {"ok": False, "detail": "empty"}
title = request.title.strip() or text.splitlines()[0][:60]
slug = _re.sub(r"[^a-zA-Z0-9]+", "-", title.lower()).strip("-") or "note"
capture_dir = settings.data_dir / "capture"
capture_dir.mkdir(parents=True, exist_ok=True)
stamp = _time.strftime("%Y%m%d-%H%M%S")
path = capture_dir / f"{stamp}-{slug}.md"
header = f"# {title}\n\n"
if request.source:
header += f"- captured from: {request.source}\n"
header += f"- captured at: {_time.strftime('%Y-%m-%d %H:%M')}\n\n"
path.write_text(header + text + "\n")
indexer.start([str(capture_dir)])
return {"ok": True, "path": str(path)}

@app.post("/feedback")
async def feedback(request: FeedbackRequest) -> dict:
if not settings.experiments:
Expand Down
11 changes: 11 additions & 0 deletions integrations/desktop/macos/Sources/CogneeDesktop/AppDelegate.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import AppKit
import Carbon.HIToolbox
import SwiftUI

@MainActor
Expand All @@ -8,6 +9,8 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
private var statusItem: NSStatusItem!
private var panelController: SearchPanelController!
private var hotKey: GlobalHotKey?
private var captureHotKey: GlobalHotKey?
private var capturePanelController: CapturePanelController?
private var settingsWindow: NSWindow?
private var inboxWindow: NSWindow?
private var shareWindow: NSWindow?
Expand All @@ -27,6 +30,14 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
self?.panelController.toggle()
}

// ⌥⇧Space: quick capture — one line straight into memory.
capturePanelController = CapturePanelController()
captureHotKey = GlobalHotKey(
modifiers: UInt32(optionKey | shiftKey), id: 2
) { [weak self] in
self?.capturePanelController?.toggle()
}

notifier.onUnseenCount = { [weak self] unseen in
self?.statusItem?.button?.toolTip =
unseen > 0 ? "Cognee — \(unseen) new learnings" : "Cognee"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,10 @@ struct BackendClient {
try await post("feedback", body: ["query": query, "answer": answer, "rating": rating])
}

func capture(text: String, title: String = "", source: String = "") async throws {
try await post("capture", body: ["text": text, "title": title, "source": source])
}

func health() async throws -> Health {
try await get(baseURL.appendingPathComponent("health"))
}
Expand Down
190 changes: 190 additions & 0 deletions integrations/desktop/macos/Sources/CogneeDesktop/CapturePanel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
import AppKit
import SwiftUI

/// ⌥⇧Space: one line into memory. Type the thought, hit ↩, keep working —
/// the note lands in the capture folder and indexes like any document.
@MainActor
final class CaptureModel: ObservableObject {
@Published var text = "" {
didSet { scheduleWhisper() }
}
@Published var status: String?
/// Memory talking back while you type: closest known fact, or a conflict.
@Published var whisper: String?
@Published var whisperIsConflict = false
var onDone: (() -> Void)?
private var whisperTask: Task<Void, Never>?

/// Debounced /whisper lookup — memory reacts to the note, not keystrokes.
private func scheduleWhisper() {
whisperTask?.cancel()
let note = text.trimmingCharacters(in: .whitespacesAndNewlines)
guard note.count >= 12 else {
whisper = nil
whisperIsConflict = false
return
}
whisperTask = Task { [weak self] in
try? await Task.sleep(nanoseconds: 500_000_000)
guard let self, !Task.isCancelled,
let response = try? await BackendClient().whisper(note),
!Task.isCancelled,
note == self.text.trimmingCharacters(in: .whitespacesAndNewlines)
else { return }
if let conflict = response.conflicts.first {
self.whisper = "conflicts with memory: \(conflict.a) vs \(conflict.b)"
self.whisperIsConflict = true
} else if let related = response.related.first {
self.whisper = "memory knows: \(related)"
self.whisperIsConflict = false
} else {
self.whisper = nil
self.whisperIsConflict = false
}
}
}

func submit() {
let note = text.trimmingCharacters(in: .whitespacesAndNewlines)
guard !note.isEmpty else {
onDone?()
return
}
status = "Remembering…"
Task { [weak self] in
do {
try await BackendClient().capture(text: note, source: "quick-capture")
self?.status = "Remembered ✓"
} catch {
self?.status = "Backend unreachable"
}
try? await Task.sleep(nanoseconds: 700_000_000)
self?.text = ""
self?.status = nil
self?.whisper = nil
self?.onDone?()
}
}
}

struct CaptureView: View {
@ObservedObject var model: CaptureModel
@FocusState private var focused: Bool

var body: some View {
VStack(spacing: 0) {
HStack(spacing: 10) {
Image(systemName: "brain")
.font(.system(size: 15, weight: .medium))
.foregroundStyle(Color.cognee)
TextField("Remember this…", text: $model.text)
.textFieldStyle(.plain)
.font(.system(size: 16))
.focused($focused)
.onSubmit { model.submit() }
if let status = model.status {
Text(status).font(.system(size: 11)).foregroundStyle(.secondary)
}
}
.padding(.horizontal, 14)
.frame(height: 44)
if let whisper = model.whisper {
Divider().padding(.horizontal, 12).opacity(0.5)
HStack(spacing: 7) {
Image(
systemName: model.whisperIsConflict
? "exclamationmark.triangle.fill" : "sparkle"
)
.font(.system(size: 10))
.foregroundStyle(
model.whisperIsConflict ? AnyShapeStyle(.orange) : AnyShapeStyle(Color.cognee)
)
Text(whisper)
.font(.system(size: 11))
.foregroundStyle(.secondary)
.lineLimit(2)
Spacer(minLength: 0)
}
.padding(.horizontal, 14)
.padding(.vertical, 7)
}
}
.frame(width: 460)
.background(.regularMaterial, in: RoundedRectangle(cornerRadius: 12, style: .continuous))
.overlay(
RoundedRectangle(cornerRadius: 12, style: .continuous)
.strokeBorder(.white.opacity(0.18), lineWidth: 1)
)
.shadow(color: .black.opacity(0.3), radius: 20, y: 8)
.padding(24)
// fixed canvas with headroom for the whisper row — the panel window
// never resizes (content outside a fitted window clips to nothing)
.frame(width: 508, height: 140, alignment: .top)
.onAppear { focused = true }
.animation(.easeOut(duration: 0.14), value: model.whisper)
}
}

@MainActor
final class CapturePanelController: NSObject, NSWindowDelegate {
private let model = CaptureModel()
private var panel: SearchPanel!
private var keyMonitor: Any?

override init() {
super.init()
panel = SearchPanel(
contentRect: NSRect(x: 0, y: 0, width: 508, height: 140),
styleMask: [.borderless, .nonactivatingPanel],
backing: .buffered,
defer: false
)
let hosting = NSHostingController(rootView: CaptureView(model: model))
hosting.sizingOptions = []
panel.contentViewController = hosting
panel.setContentSize(NSSize(width: 508, height: 140))
panel.isOpaque = false
panel.backgroundColor = .clear
panel.hasShadow = false
panel.level = .floating
panel.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary, .transient]
panel.delegate = self
model.onDone = { [weak self] in self?.hide() }
}

func toggle() {
panel.isVisible ? hide() : show()
}

private func show() {
let screen =
NSScreen.screens.first { NSMouseInRect(NSEvent.mouseLocation, $0.frame, false) }
?? NSScreen.main
if let frame = screen?.visibleFrame {
panel.setFrameOrigin(
NSPoint(
x: frame.midX - panel.frame.width / 2,
y: frame.minY + frame.height * 0.78
))
}
panel.makeKeyAndOrderFront(nil)
keyMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { [weak self] event in
guard let self, self.panel.isKeyWindow else { return event }
if event.keyCode == 53 { // esc
self.hide()
return nil
}
return event
}
}

private func hide() {
if let keyMonitor { NSEvent.removeMonitor(keyMonitor) }
keyMonitor = nil
panel.orderOut(nil)
}

func windowDidResignKey(_ notification: Notification) {
hide()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,18 @@ final class GlobalHotKey {
private var hotKeyRef: EventHotKeyRef?
private var eventHandler: EventHandlerRef?
private let handler: () -> Void
private let id: UInt32

/// Default binding: Option+Space (Command+Space stays with the system search).
/// Each registered hotkey needs its own ``id``.
init?(
keyCode: UInt32 = UInt32(kVK_Space),
modifiers: UInt32 = UInt32(optionKey),
id: UInt32 = 1,
handler: @escaping () -> Void
) {
self.handler = handler
self.id = id

var eventType = EventTypeSpec(
eventClass: OSType(kEventClassKeyboard),
Expand All @@ -25,9 +29,26 @@ final class GlobalHotKey {
var installedHandler: EventHandlerRef?
let installStatus = InstallEventHandler(
GetApplicationEventTarget(),
{ _, _, userData -> OSStatus in
guard let userData else { return noErr }
{ _, event, userData -> OSStatus in
guard let userData, let event else { return noErr }
// Every installed handler sees every hotkey press; without this
// check, whichever hotkey fires triggers ALL registered actions
// (and the last-installed one swallows the event). Dispatch on
// the pressed hotkey's ID and pass foreign events along.
var pressedID = EventHotKeyID()
GetEventParameter(
event,
EventParamName(kEventParamDirectObject),
EventParamType(typeEventHotKeyID),
nil,
MemoryLayout<EventHotKeyID>.size,
nil,
&pressedID
)
let hotKey = Unmanaged<GlobalHotKey>.fromOpaque(userData).takeUnretainedValue()
guard pressedID.id == hotKey.id else {
return OSStatus(eventNotHandledErr)
}
DispatchQueue.main.async { hotKey.handler() }
return noErr
},
Expand All @@ -39,7 +60,7 @@ final class GlobalHotKey {
guard installStatus == noErr else { return nil }
eventHandler = installedHandler

let hotKeyID = EventHotKeyID(signature: OSType(0x4347_5350), id: 1) // 'CGSP'
let hotKeyID = EventHotKeyID(signature: OSType(0x4347_5350), id: id) // 'CGSP'
var registeredRef: EventHotKeyRef?
let registerStatus = RegisterEventHotKey(
keyCode, modifiers, hotKeyID, GetApplicationEventTarget(), 0, &registeredRef
Expand Down
Loading