Skip to content

feat: agent mode tool-calling via Vercel AI SDK#451

Merged
gabrielste1n merged 96 commits into
mainfrom
feat/agent-tool-calling
Apr 2, 2026
Merged

feat: agent mode tool-calling via Vercel AI SDK#451
gabrielste1n merged 96 commits into
mainfrom
feat/agent-tool-calling

Conversation

@gabrielste1n

@gabrielste1n gabrielste1n commented Mar 16, 2026

Copy link
Copy Markdown
Collaborator

Summary

Full agent mode implementation with Vercel AI SDK tool-calling, embedded chat, local semantic search, and comprehensive UX polish.

Agent & Chat System

  • Migrate agent mode to Vercel AI SDK with full tool-calling (search notes, create/update notes, web search)
  • Add sidebar chat tab with conversation history, cloud sync, and semantic search
  • Embedded chat panel in note editor with floating and sidebar dock modes
  • Redesign thinking/streaming state with shimmer gradient indicator and stop button
  • Stream cloud agent responses via NDJSON with rich tool step visualization
  • Enable local model tool calling with RAG context injection

Local Semantic Search

  • Always-on offline semantic search via Qdrant sidecar (cosine similarity, 384-dim MiniLM embeddings)
  • Hybrid search: FTS5 + Qdrant in parallel with Reciprocal Rank Fusion (K=60)
  • Auto-index notes on create/update/delete, auto-download embedding model on first launch

Notes & Meeting Improvements

  • Add Actions sidebar button and redesign action editor dialog (master-detail two-panel layout)
  • Add folder picker to note metadata row, new note dialog with folder selection
  • Prevent meeting view from exiting when changing folders
  • Fix auto-detection not firing for browser meetings
  • Fix partial transcript spam and duplicate final segments
  • Use local transcription provider for notes/meeting recording
  • Enable echo cancellation on mic stream

Linux & Platform Fixes

  • Force XWayland on KDE/GNOME Wayland for window positioning
  • Use uinput before portal on GNOME Wayland
  • Fix hotkey startup race condition

UI Polish

  • Align all dialogs with design system guidelines
  • Reuse CommandSearch for chat search
  • Increase sidebar search and note metadata visibility
  • Remove vertical accent bar selection indicators

Transform Agent Mode from text-only chat into a full agentic experience
with native tool/function calling. The agent can now search notes, copy
to clipboard, search the web (via cloud API), and check calendar events.

- Tool registry with OpenAI-compatible function calling format
- ReAct execution loop with parallel read-only tool execution
- SSE streaming with incremental tool call argument accumulation
- Inline tool execution UI (compact pills with status animations)
- Text input field alongside voice input for tool-heavy workflows
- Dynamic system prompt with tool usage instructions
- IPC handler for web search via OpenWhispr cloud API
- Database migration for tool message metadata
- i18n strings for all 10 supported locales
Replace manual SSE parsing and AgentLoop with AI SDK streamText +
stepCountIs for tool-calling agent mode. Add unified provider factory
supporting OpenAI, Groq, Anthropic, Gemini, and custom endpoints.

- Add ai, @ai-sdk/openai, @ai-sdk/groq, @ai-sdk/anthropic, @ai-sdk/google
- Add src/services/ai/providers.ts with getAIModel factory
- Add ToolRegistry.toAISDKFormat() using jsonSchema wrapper
- Add ReasoningService.processTextStreamingAI() with full tool support
- Remove AgentLoop.ts (replaced by stepCountIs)
- Remove dead toOpenAIFormat/OpenAIFunctionTool from ToolRegistry
- Simplify AgentOverlay to 3 streaming paths (tools/cloud/BYOK)
…isableThinking

Uses AI SDK's providerOptions API to send reasoning_effort: "none" to
Groq for models flagged with disableThinking in the model registry.
- Add mounted ref to guard state updates after overlay unmount
- Add AudioManager.cleanup() call on unmount
- Handle tool-result stream chunks to show actual results in UI
- Set tool status to "executing" until result arrives (fix state thrashing)
- Add try/catch in ToolRegistry.toAISDKFormat() execute wrapper
- Extend AgentStreamChunk type with tool_result variant
- Add success field to IPC web-search response for contract consistency
- Add get_note tool to fetch full note content by ID
- Add create_note tool with folder resolution and cloud sync
- Add update_note tool for title, content, and folder changes
- Add shared resolveFolderId utility for folder name lookup
- Include note ID in search_notes results for cross-tool reference
- Register tools and add system prompt instructions
- Add translation keys for all 10 locales
- Enable tool calling for cloud agent mode (clipboard, notes, web search, calendar)
- Implement NDJSON streaming via IPC batch approach for reliable event delivery
- Add multi-step tool-calling loop with AI SDK v6 message format
- Fix mountedRef StrictMode bug causing empty renders
- Return actual tool result data to LLM instead of just display text
- Extract MAX_TOOL_STEPS constant, remove redundant comments
When cloud backup is enabled and user is signed in, the search_notes
agent tool now uses the cloud hybrid search (pgvector + FTS) instead
of local SQLite FTS5 keyword search. Falls back to local search
transparently on cloud failure.
Replace the buffered IPC middleman with direct fetch from the renderer
to the API. Text now streams token-by-token instead of arriving all at
once after the full response completes. Adds AbortController support for
instant cancellation on unmount or user stop.
The renderer's fetch() can't access auth cookies stored on the Neon Auth
domain. Add a get-session-cookies IPC handler to retrieve them from the
main process cookie jar and forward as an explicit Cookie header.
Browser fetch() forbids setting the Cookie header, so direct renderer-to-
API streaming can't authenticate. Switch to event-based IPC: main process
reads the API stream and forwards each chunk via webContents.send() as it
arrives. This matches the pattern used for AssemblyAI/Deepgram streaming.
Replace basic tool pills with step-based visualization showing the full
tool lifecycle: shimmer accent while executing, checkmark pop on
completion, expandable detail, and contextual clipboard confirmation.

- ToolCallStep: left-border accent, per-tool icons, shimmer animation
- Clipboard: inline "Copied to your clipboard" with green check
- Input bar: thinking shimmer bar, tool icon in executing state
- Empty state: mic icon with dual-line CTA
- Title bar: shows agent name, softer shadow
- Tighter spacing throughout (gaps, heights, bubble widths)
- New CSS: tool-step-shimmer, tool-check-pop, tool-status-sweep
- i18n: copiedToClipboard + orType in all 10 locales
…ults

The executeToolCall callback was returning raw data (full JSON) which was
displayed in the tool step UI. Now returns { data, displayText } so the
LLM gets structured data while the UI shows a human-readable summary.

Also truncates Exa web search article text to 500 chars per result to
prevent massive payloads.
- Increase MAX_TOOL_STEPS from 5 to 20 to prevent agent from getting
  stuck on multi-step workflows
- Add metadata field to ToolCallInfo so tool results can carry structured
  data (e.g. note ID) to the UI without mixing it into displayText
- Created/updated/fetched notes show as clickable steps with a primary
  accent — clicking opens the note in the control panel
- New agent-open-note IPC handler navigates the control panel to the note
- Note cards: when create/update/get_note completes, render a compact
  clickable card at the bottom of the message bubble with title, icon,
  and "Open note" label. Clicking opens the note in the control panel.
- Input bar: replace generic loading dots with dictation-panel-inspired
  indicators — pulsing blue circle for listening, accent wave bars for
  transcribing, shimmer sweep for thinking.
- Fix duplicate copiedToClipboard keys in all locale files.
…mode

The agent-open-note IPC was reusing navigate-to-meeting-note which
activates meeting recording mode. Add a dedicated navigate-to-note event
that only sets the active note and view without starting the recorder.
Resolved conflict in preload.js — kept both onNavigateToNote
(feature branch) and update notification APIs (main).
- Add AbortController with 60s timeout to streaming fetch in
  ReasoningService (prevents hanging on network failure)
- Wire cancelActiveStream() to abort the streaming controller
- Add isProcessing guard to processWithAnthropic and processWithLocal
  to prevent concurrent request overlap
- Wrap JSON.parse(argsJson) in try/catch in agent tool executor to
  gracefully handle malformed LLM tool arguments
- Fix calendar "tomorrow" window: compute minutes until end-of-tomorrow
  instead of hardcoded 2880 (48h) which over-fetched into the day after
- Replace hardcoded English strings in agent chat with i18n t() calls:
  "New conversation", "Unknown tool:", "Error:" prefix
- Add unknownTool and invalidArgs translation keys to all 10 locales
- Translate pricing feature arrays in all 9 non-English locales
- Translate enterprise subtitle and CTA in all 9 non-English locales
- Fix dark mode: replace raw bg-emerald-100/text-emerald-600 with
  opacity-based bg-green-500/10 and text-green-500 in OnboardingFlow
- Remove duplicate ToolCallEntry from AgentOverlay, import Message/ToolCallInfo from AgentChat
- Thread metadata through addAgentMessage (database → IPC → preload → types → call sites)
- Add StreamMessage type for tool role + array content at API boundary
- Memoize tool registry via ref, invalidate on settings change or new chat
…ission checks

IntegrationsView and UpcomingMeetings were checking systemAudio.status === "denied"
which missed "not-determined" and "unknown" states — causing the permission gate to
be skipped when it shouldn't be. Now uses mode === "native" && !granted, matching
the pattern already used in SettingsPage and PermissionsSection.

Also changed both flows to call systemAudio.request() (native consent dialog) instead
of systemAudio.openSettings (jumps straight to System Settings).
…apid delete/switch

Move next-note selection into removeNote (uses fresh getState), flush
pending debounced saves on note switch, use refs instead of stale
closures in title/content change handlers, and clear local editor state
on note deletion.
Adds offline semantic search using a local Qdrant vector DB sidecar and
MiniLM ONNX embeddings (384-dim). Notes are indexed on create/update/delete
and searched via Reciprocal Rank Fusion (FTS5 + vector). Gracefully
degrades to FTS5-only when Qdrant is unavailable.

- Qdrant binary management: download script + process lifecycle manager
- Local embedding pipeline: WordPiece tokenizer + ONNX inference
- Vector index: Qdrant collection CRUD + batch reindex
- Hybrid search: RRF merge with 0.3 cosine threshold filter
- Search tool: three-tier fallback (cloud → local semantic → FTS5)
- Settings toggle in Privacy & Data with i18n (10 languages)
- .env persistence for LOCAL_SEMANTIC_SEARCH across restarts
Add architecture docs, dev setup instructions, troubleshooting guide,
settings reference, and testing checklist for the Qdrant sidecar and
MiniLM embedding pipeline.
Semantic search is no longer optional — Qdrant starts unconditionally on
app launch if the binary is available. Embedding model auto-downloads on
first run. Qdrant binary downloads automatically in predev/prestart.

Removes: settings toggle, localSemanticSearchEnabled setting, i18n keys,
enable/disable IPC handlers, LOCAL_SEMANTIC_SEARCH env var.
- Add qdrant-* to extraResources filter so binary is bundled in release
- Add onnxruntime-node to asarUnpack (native module needs unpacking)
- Fix localEmbeddings.downloadModel() to use runtime downloadUtils.js
  instead of build-time scripts/lib/download-utils (not packaged in ASAR)
Add download-qdrant.js step to linux, windows, and macOS jobs in both
build-and-notarize.yml and release.yml. Passes --platform/--arch for
macOS cross-compilation (same pattern as whisper-cpp and sherpa-onnx).
processTranscription() was missing the skipReasoning check, so BYOK
transcription paths (local whisper, parakeet, OpenAI API, Mistral)
still ran LLM cleanup before sending text to the agent chat. Only the
OpenWhispr Cloud and streaming paths had the guard. Add early return
in processTranscription() so agent mode always gets raw transcription.
…chips

Enable browser-level echo cancellation (AEC) on the meeting microphone
stream so macOS CoreAudio subtracts speaker output from mic input,
eliminating duplicate transcription when not using headphones.

Add metadata chips to the note editor showing date, linked calendar
event, folder, and meeting type. Add gcalGetEvent IPC to fetch calendar
event details for display.

Update CLAUDE.md and SECURITY.md to reflect current API key storage
(plaintext, not yet encrypted — tracked in #532).
Make the folder metadata chip clickable with a dropdown picker matching
the sidebar's move-to-folder UX. Remove parchment background from light
mode inputs, align folder picker input styles across both dropdowns, and
bump export button contrast.
Add generation counter to initializeNotes so stale IPC responses from
earlier folder clicks are discarded. Clear active note immediately on
folder switch and double-guard post-load note selection. Fix title
flicker by using note's own data on the first render before local state
syncs. Await save flush when switching notes to prevent data races.
Fix stale closure in handleDeleteFolder by reading fresh folder data.
…ments

Guard against empty OpenAI Realtime deltas firing onPartialTranscript
repeatedly, and skip onFinalTranscript when the completed transcript is
empty to avoid re-sending the previous segment.
Skip re-filtering the note list by folder during meeting mode so the
active note stays visible and the recording continues uninterrupted.
Make search input border transparent with stronger outline, and bump
metadata button/text opacity for better readability in both themes.
…rding (#530)

Meeting transcription always used the cloud OpenAI Realtime pipeline,
ignoring the user's local transcription setting. When local mode was
selected (Parakeet or Whisper), the app tried to authenticate with the
cloud and failed with "No session cookies available."

Route meeting transcription through local engines when useLocalWhisper
is enabled. Buffer 5-second audio chunks, downsample from 24kHz to
16kHz for local engine compatibility, and gate on RMS energy to prevent
Whisper hallucinations on silent chunks.
Use kAudioObjectPropertyScopeGlobal instead of kAudioObjectPropertyScopeInput
for CoreAudio property listeners in macos-mic-listener. The input scope was not
receiving property change notifications when browsers like Chrome activate the
mic for Google Meet. Add a 5s heartbeat poll as safety net.

Reset hasPrompted on the audio activity detector when exiting meeting mode so
future detections are not silently blocked.

Add diagnostic logging to suppression paths in _onMicStateChanged.
…alog

Redesign ActionManagerDialog with master-detail two-panel layout for
better prompt editing UX. Add Actions button to notes sidebar for
quick access. Add i18n keys across all 10 locales.
electron-builder v26 requires publisherName in azureSignOptions.
Added as env var to config and both CI workflows.
Standard platform convention: ⌘, on macOS (Apple HIG), Ctrl+, on
Windows/Linux. Adds "Settings..." menu item with accelerator to app
menu (macOS) and File menu (Windows/Linux), IPC bridge to renderer,
and fallback keydown listener alongside existing ⌘K handler.
…r download

The PR build step was failing because azureSignOptions in electron-builder.json
triggers Azure Trusted Signing even without auth env vars. Merged the two
conditional build steps into one that always passes signing credentials.

Also adds the missing windows-mic-listener download step to both CI workflows
(was in prebuild:win but missing from CI), and fixes inconsistent GITHUB_TOKEN
fallback on windows-fast-paste and windows-key-listener download steps.
Remove the PermissionsGate that blocked the entire UI when microphone
permission wasn't granted. Users can now browse notes, settings, and
use system-audio-only recording without mic access. Mic permission is
still requested at point-of-use (recording, device enumeration) with
clear error toasts when denied.

Fix system audio permission detection on macOS by replacing the broken
systemPreferences.getMediaAccessStatus("screen") check (which queried
screen-capture TCC, not system-audio TCC) with a cached status from
AudioTapManager.checkAccess(). The status resolves after the user
clicks "Grant Access" or completes a recording session, without
triggering unexpected system dialogs on passive checks.
One-way write-through from SQLite to .md files on disk, organized by
folder. Includes settings toggle, custom save location picker, rebuild
button, and "Show in Finder/Explorer/Files" context menu items on notes
and folders. Off by default; first enable triggers full rebuild.
electron-builder's ${env.} interpolation is not supported for
azureSignOptions fields, causing literal strings to be passed to
Invoke-TrustedSigning instead of resolved values. Hardcode the
non-secret resource identifiers directly in the config and remove
the now-unnecessary env var passthrough from workflows.
Add responsive layout to the settings dialog that adapts when the
window is narrow (< 800px), such as during meeting recorder mode at
1/3 screen width.

- SidebarModal: collapse sidebar from 208px to 48px icon-only rail
  using ResizeObserver with callback ref (for Radix Portal compat),
  propagate compact state via SettingsLayoutContext
- SettingsRow: stack vertically in compact mode (label above control)
- SettingsPanelRow: reduce padding in compact mode
- Plans grid: switch from 4 to 2 columns in compact mode
- DeveloperSection: switch grid from 2 to 1 column in compact mode
- ReasoningModelSelector: add break-all on URL code blocks
- NoteBottomBar: use primary color tokens for recorder controls
Probe the native audio tap binary once during deferred init to cache
the real TCC status for the session. Uses getMediaAccessStatus("screen")
as a guard — only probes when the user has been asked before (not
"not-determined"), so first-time users never see an unexpected consent
dialog. Fixes the card showing "Grant Access" after restart even when
permission was already granted.
Remove the getMediaAccessStatus("screen") guard from resolvePermission
— it checks kTCCServiceScreenCapture (wrong TCC entry), not
kTCCServiceAudioCapture which process taps actually use. The macOS
consent dialog only appears on the very first call to
AudioHardwareCreateProcessTap; subsequent calls are always silent.
Trim excess comments from IPC handler.
The marker file + 3 helper methods protected against a non-problem:
new users seeing the macOS consent dialog slightly before onboarding.
This is standard macOS behavior (Zoom, Discord, Chrome all do it).
Revert to a simple probe on startup.
… probe

Replace the startup probe (which could trigger the macOS consent dialog
without user action) with file-based persistence in userData. The
permission status is written to .system-audio-permission when the user
explicitly clicks "Grant Access" or when a recording resolves the
status. On next launch, the constructor reads the cached value — no
probe, no dialog, correct UI state immediately.

Follows the same persistence pattern as mic permission (renderer uses
localStorage, main process uses userData file).
Remove PermissionsGate.tsx — no longer imported after the gate was
removed from main.jsx. Simplify requestAccess catch handler.
@gabrielste1n
gabrielste1n merged commit 03fdc07 into main Apr 2, 2026
4 of 6 checks passed
@gabrielste1n
gabrielste1n deleted the feat/agent-tool-calling branch April 2, 2026 19:14
@slumbi

slumbi commented Apr 14, 2026

Copy link
Copy Markdown

Size alert: due to this MR, the 1.6.7 Linux tarball nearly doubles (185MB426MB). Major additions are bundled CUDA ONNX Runtime libs (libonnxruntime_providers_cuda.so ~316MB, libonnxruntime.so.1 35MB) plus qdrant-linux-x64 96MB.

Was this really necessary ?

charian47 pushed a commit to charian47/openwhispr that referenced this pull request Apr 25, 2026
…ling

feat: agent mode tool-calling via Vercel AI SDK
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants