Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
19 changes: 14 additions & 5 deletions packages/koe-core/src/providers/groq-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,34 @@ import { DEFAULT_CUSTOM_PROMPT } from '../constants';
/**
* Resolves the prompt to use for AI enhancement based on the selected style.
*/
export function resolveEnhancementPrompt(promptStyle: string = 'Clean', customPrompt: string = ''): string {
export function resolveEnhancementPrompt(promptStyle: string = 'Clean', customPrompt: string = '', smartContext: string = ''): string {
const contextPrefix = smartContext ? `Context: ${smartContext}. ` : '';
const trimmedPrompt = customPrompt.trim();
if (trimmedPrompt) {
return trimmedPrompt;
Comment on lines +7 to 10
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

smartContext is ignored when customPrompt is set.

The early return bypasses the new context prefix entirely, so Smart Context silently stops working for custom-prompt users.

🧩 Minimal fix
     const trimmedPrompt = customPrompt.trim();
     if (trimmedPrompt) {
-        return trimmedPrompt;
+        return contextPrefix + trimmedPrompt;
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const contextPrefix = smartContext ? `Context: ${smartContext}. ` : '';
const trimmedPrompt = customPrompt.trim();
if (trimmedPrompt) {
return trimmedPrompt;
const contextPrefix = smartContext ? `Context: ${smartContext}. ` : '';
const trimmedPrompt = customPrompt.trim();
if (trimmedPrompt) {
return contextPrefix + trimmedPrompt;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/koe-core/src/providers/groq-helpers.ts` around lines 7 - 10, The
current early return in the function ignores smartContext by returning
trimmedPrompt directly; update the return path so when customPrompt
(trimmedPrompt) is present you prepend the computed contextPrefix (based on
smartContext) before returning (e.g., return contextPrefix + trimmedPrompt),
ensuring you still trim customPrompt and preserve spacing and punctuation so
smartContext is not silently dropped; adjust logic around contextPrefix,
trimmedPrompt and the return in the function handling custom prompts.

}

if (promptStyle === 'Professional' || promptStyle === 'Formal') {
return 'Refine this dictated text with a formal, professional tone. Keep the meaning intact, fix punctuation and grammar, remove filler only when it is clearly filler, never use em dashes, and do not add transcript tags or any other wrapper markup.';
return contextPrefix + 'Refine this dictated text with a formal, professional tone. Keep the meaning intact, fix punctuation and grammar, remove filler only when it is clearly filler, never use em dashes, and do not add transcript tags or any other wrapper markup.';
}

if (promptStyle === 'Casual') {
return 'Refine this dictated text so it stays casual and conversational. Keep the meaning intact, fix punctuation and grammar, remove filler only when it is clearly filler, never use em dashes, and do not add transcript tags or any other wrapper markup.';
return contextPrefix + 'Refine this dictated text so it stays casual and conversational. Keep the meaning intact, fix punctuation and grammar, remove filler only when it is clearly filler, never use em dashes, and do not add transcript tags or any other wrapper markup.';
}

if (promptStyle === 'Concise' || promptStyle === 'Bullets') {
return 'Refine this dictated text into a tighter version with less filler while keeping the original meaning. Remove filler words like um, uh, and obvious filler mistranscriptions like ohms only when they are clearly filler, not when they are literal or technical. Never use em dashes, and do not add transcript tags or any other wrapper markup.';
return contextPrefix + 'Refine this dictated text into a tighter version with less filler while keeping the original meaning. Remove filler words like um, uh, and obvious filler mistranscriptions like ohms only when they are clearly filler, not when they are literal or technical. Never use em dashes, and do not add transcript tags or any other wrapper markup.';
}

return DEFAULT_CUSTOM_PROMPT;
if (promptStyle === 'Meeting Notes' || promptStyle === 'Meeting') {
return contextPrefix + 'Refine this meeting transcript into a structured set of meeting notes. Include a summary of the main points and a clear list of action items with owners if mentioned. Maintain a professional tone and ensure the output is concise and actionable.';
}

if (promptStyle === 'Nigerian Pidgin') {
return contextPrefix + 'Refine this dictated text in Nigerian Pidgin English. Maintain the authentic Pidgin grammar and flow. Use standard Pidgin spellings (e.g., "sabi", "wetin", "pikin", "don", "go", "dey"). Fix punctuation and remove obvious verbal filler while preserving the original meaning and vibrant tone. Do not translate it to standard English.';
}

return contextPrefix + DEFAULT_CUSTOM_PROMPT;
}

export function parseErrorMessage(payload: any, fallback: string): string {
Expand Down
24 changes: 23 additions & 1 deletion src/main/ipc.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const { retryAndPasteTranscript } = require('./services/retry-transcript');
const pendingRetryService = require('./services/pending-retry');
const sessionManager = require('./services/transcription-session-manager');
const { closeSettingsWindow } = require('./settings-window');
const { updateHotkey } = require('./shortcuts');
const { updateHotkey, handleRecordingToggle } = require('./shortcuts');
const { applyLaunchOnStartupPreference } = require('./services/startup');
const { applyAutoUpdatePreference } = require('./services/updater');
const logger = require('./services/logger');
Expand Down Expand Up @@ -74,8 +74,25 @@ function setupIpcHandlers(mainWindow) {
ipcMain.handle(CHANNELS.TEST_GROQ_KEY, async (event, apiKey) => validateApiKey(apiKey));
ipcMain.handle(CHANNELS.GET_USAGE_STATS, async () => rateLimiter.getUsageStats());
ipcMain.handle(CHANNELS.GET_HISTORY, async () => historyService.getHistory());
ipcMain.handle(CHANNELS.SEARCH_HISTORY, async (event, query) => historyService.searchHistory(query));
ipcMain.handle(CHANNELS.CLEAR_HISTORY, async () => historyService.clearHistory());

ipcMain.handle(CHANNELS.GET_TASKS, async () => {
const historyStore = new Store({ name: 'tasks-history' });
return historyStore.get('tasks', []);
});

ipcMain.handle(CHANNELS.TOGGLE_TASK, async (event, taskId) => {
const historyStore = new Store({ name: 'tasks-history' });
const tasks = historyStore.get('tasks', []);
const task = tasks.find(t => t.id === taskId);
if (task) {
task.completed = !task.completed;
historyStore.set('tasks', tasks);
}
return tasks;
Comment on lines +80 to +93
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Expectation:
# - `new Store(` matches are present
# - no `require('electron-store')` match is present in src/main/ipc.js
rg -n "new Store\\(|require\\(['\"]electron-store['\"]\\)" src/main/ipc.js

Repository: JStaRFilms/Koe

Length of output: 197


🏁 Script executed:

head -30 src/main/ipc.js

Repository: JStaRFilms/Koe

Length of output: 1291


Store is undefined in the new task handlers.

The module never imports electron-store, so both GET_TASKS and TOGGLE_TASK handlers will throw ReferenceError: Store is not defined when called.

Fix
const logger = require('./services/logger');
const fs = require('fs');
+const Store = require('electron-store');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/ipc.js` around lines 80 - 93, The handlers for CHANNELS.GET_TASKS
and CHANNELS.TOGGLE_TASK reference Store but the module never imports it; add an
import/require for electron-store (i.e., ensure Store is defined) at the top of
src/main/ipc.js so the ipcMain.handle callbacks can construct new Store({ name:
'tasks-history' }) without throwing; update the file to import Store (used by
the GET_TASKS and TOGGLE_TASK handlers) before those handlers are registered.

});

ipcMain.handle(CHANNELS.RETRY_HISTORY_ENTRY, async (event, entryId) => {
return retryAndPasteTranscript(entryId, {
beforePaste: hideSettingsBeforePaste
Expand Down Expand Up @@ -114,6 +131,11 @@ function setupIpcHandlers(mainWindow) {
closeSettingsWindow();
});

ipcMain.on(CHANNELS.TOGGLE_RECORDING, (event, options) => {
handleRecordingToggle(mainWindow, options);
});


ipcMain.on(CHANNELS.AUDIO_SEGMENT, (event, audioData) => {
sessionManager.handleSegment(audioData).catch((error) => {
logger.error('[Pipeline] Failed to enqueue audio segment:', error);
Expand Down
2 changes: 2 additions & 0 deletions src/main/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const { setupIpcHandlers } = require('./ipc');
const { createSettingsWindow } = require('./settings-window');
const { getPillBounds, pinPillWindow } = require('./services/pill-window');
const { getSetting } = require('./services/settings');
const meetingDetector = require('./services/meeting-detector');
const { applyLaunchOnStartupPreference } = require('./services/startup');
const { applyAutoUpdatePreference } = require('./services/updater');
const sessionManager = require('./services/transcription-session-manager');
Expand Down Expand Up @@ -81,6 +82,7 @@ app.whenReady().then(() => {
setupTray(mainWindow);
registerShortcuts(mainWindow);
setupIpcHandlers(mainWindow);
meetingDetector.init(mainWindow);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Resource leak: meetingDetector interval is never stopped.

meetingDetector.init() starts a setInterval polling loop (every 30 seconds) that is never cleaned up. The stopMonitoring() method exists but isn't called in before-quit. This causes:

  1. The interval to continue running after the app begins quitting
  2. Potential attempts to send IPC to a destroyed window
Proposed fix
 app.on('before-quit', () => {
     app.isQuitting = true;
     unregisterShortcuts();
+    meetingDetector.stopMonitoring();
     sessionManager.shutdown();
 });

Also applies to: 106-110

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/main.js` at line 85, The meetingDetector.init(mainWindow) call
starts a recurring interval that is never cleared; update app shutdown handling
to call meetingDetector.stopMonitoring() (the existing cleanup method) during
the 'before-quit' lifecycle event (and also when the main window is
closed/destroyed) so the interval is cleared and no further IPC is attempted to
a destroyed mainWindow; ensure you reference meetingDetector.init,
meetingDetector.stopMonitoring, the 'before-quit' event handler and any
mainWindow 'closed' listener to perform this cleanup.


applyLaunchOnStartupPreference(getSetting('launchOnStartup') !== false);
applyAutoUpdatePreference(getSetting('autoUpdate') !== false);
Expand Down
14 changes: 14 additions & 0 deletions src/main/preload.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,16 @@ contextBridge.exposeInMainWorld('api', {
ipcRenderer.on(CHANNELS.WINDOW_ANIMATE_IN, () => callback());
},

onMeetingDetected: (callback) => {
ipcRenderer.removeAllListeners(CHANNELS.MEETING_DETECTED);
ipcRenderer.on(CHANNELS.MEETING_DETECTED, () => callback());
},

onAiInsight: (callback) => {
ipcRenderer.removeAllListeners(CHANNELS.AI_INSIGHT);
ipcRenderer.on(CHANNELS.AI_INSIGHT, (event, insight) => callback(insight));
},

// Settings
getSettings: () => ipcRenderer.invoke(CHANNELS.GET_SETTINGS),
saveSettings: (settings) => ipcRenderer.invoke(CHANNELS.SAVE_SETTINGS, settings),
Expand All @@ -30,15 +40,19 @@ contextBridge.exposeInMainWorld('api', {

// History
getHistory: () => ipcRenderer.invoke(CHANNELS.GET_HISTORY),
searchHistory: (query) => ipcRenderer.invoke(CHANNELS.SEARCH_HISTORY, query),
clearHistory: () => ipcRenderer.invoke(CHANNELS.CLEAR_HISTORY),
retryHistoryEntry: (entryId) => ipcRenderer.invoke(CHANNELS.RETRY_HISTORY_ENTRY, entryId),
retryLastTranscript: () => ipcRenderer.invoke(CHANNELS.RETRY_LAST_TRANSCRIPT),
exportHistory: (format) => ipcRenderer.invoke('history:export', format),
getTasks: () => ipcRenderer.invoke(CHANNELS.GET_TASKS),
toggleTask: (taskId) => ipcRenderer.invoke(CHANNELS.TOGGLE_TASK, taskId),

// Logs
openLogsFolder: () => ipcRenderer.invoke('app:open-logs'),

// Audio
toggleRecording: (options = {}) => ipcRenderer.send(CHANNELS.TOGGLE_RECORDING, options),
sendAudioSegment: (payload) => ipcRenderer.send(CHANNELS.AUDIO_SEGMENT, payload),
sendAudioChunk: (payload) => ipcRenderer.send(CHANNELS.AUDIO_SEGMENT, payload),
notifyAudioSessionStopped: (payload) => ipcRenderer.send(CHANNELS.AUDIO_SESSION_STOPPED, payload),
Expand Down
19 changes: 19 additions & 0 deletions src/main/services/email-service.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
const logger = require('./logger');

class EmailService {
async sendMeetingSummary(to, summary) {
if (!to) {
logger.warn('[EmailService] No email address provided for summary.');
return;
}

// Simulating email sending
logger.info(`[EmailService] Sending meeting summary to ${to}...`);
logger.debug('[EmailService] Summary content:', summary);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

PII exposure: meeting summary logged at debug level.

The summary may contain sensitive meeting content (names, discussions, action items). Logging it even at debug level could expose PII in log files.

Proposed fix
-        logger.debug('[EmailService] Summary content:', summary);
+        logger.debug('[EmailService] Summary length:', summary?.length || 0);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
logger.debug('[EmailService] Summary content:', summary);
logger.debug('[EmailService] Summary length:', summary?.length || 0);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/services/email-service.js` at line 12, The debug log currently
prints the full meeting summary (logger.debug('[EmailService] Summary content:',
summary)) which may contain PII; remove the direct logging of the summary in
EmailService and instead log non-PII metadata (e.g., whether a summary exists,
its length, or a redacted/hashed marker) or use an existing redaction utility
before logging; update any tests or callers that expect the full content to be
logged.


// In a real app, you'd use a service like SendGrid, Mailgun, or Nodemailer
return Promise.resolve({ success: true });
}
Comment on lines +3 to +16
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Stub implementation with sendEmailSummaries defaulting to true may mislead users.

This is a placeholder that doesn't actually send emails, but DEFAULT_SETTINGS.sendEmailSummaries is true (per src/shared/constants.js:59). Users who configure their email address will expect summaries to be delivered.

Consider either:

  1. Setting sendEmailSummaries: false by default until real email integration is implemented
  2. Adding a clear UI indication that email sending is not yet functional
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/services/email-service.js` around lines 3 - 16, DEFAULT_SETTINGS
currently defaults sendEmailSummaries to true which is misleading because
EmailService.sendMeetingSummary is a stub; change the default to false in
DEFAULT_SETTINGS.sendEmailSummaries and update EmailService.sendMeetingSummary
to explicitly log that email sending is a stub (e.g., "[EmailService] Email
delivery not implemented — summary not sent") so users and devs see the
behavior; reference the EmailService class and sendMeetingSummary method and the
DEFAULT_SETTINGS.sendEmailSummaries symbol when making these edits.

}

module.exports = new EmailService();
12 changes: 12 additions & 0 deletions src/main/services/history.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,17 @@ function getEntryRawText(entry) {
return entry?.rawText || entry?.text || '';
}

function searchHistory(query) {
const entries = getHistory();
if (!query) return entries;

const lowerQuery = query.toLowerCase();
return entries.filter(entry =>
(entry.refinedText || '').toLowerCase().includes(lowerQuery) ||
(entry.rawText || '').toLowerCase().includes(lowerQuery)
);
}

function clearHistory() {
getStore().set('entries', []);
return [];
Expand All @@ -86,5 +97,6 @@ module.exports = {
getHistoryEntryById,
getLatestEntry,
getEntryRawText,
searchHistory,
clearHistory
};
82 changes: 82 additions & 0 deletions src/main/services/meeting-detector.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
const { exec } = require('child_process');
const { Notification } = require('electron');
const logger = require('./logger');

const MEETING_PROCESSES = [
'Zoom.exe',
'Teams.exe',
'ms-teams.exe',
'Slack.exe',
'Webex.exe'
];
Comment on lines +5 to +11
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Meeting process detection only works on Windows.

The MEETING_PROCESSES array contains Windows executable names (.exe suffix). On macOS/Linux, ps -e returns different process names:

  • macOS: zoom.us, Microsoft Teams, Slack, Webex
  • Linux: zoom, teams, slack, etc.

The current implementation will never detect meetings on non-Windows platforms.

Proposed fix for cross-platform support
-const MEETING_PROCESSES = [
-    'Zoom.exe',
-    'Teams.exe',
-    'ms-teams.exe',
-    'Slack.exe',
-    'Webex.exe'
-];
+const MEETING_PROCESSES_WIN = [
+    'Zoom.exe',
+    'Teams.exe',
+    'ms-teams.exe',
+    'Slack.exe',
+    'Webex.exe'
+];
+
+const MEETING_PROCESSES_MAC = [
+    'zoom.us',
+    'Microsoft Teams',
+    'Slack',
+    'Webex'
+];
+
+const MEETING_PROCESSES_LINUX = [
+    'zoom',
+    'teams',
+    'slack',
+    'webex'
+];
+
+function getMeetingProcesses() {
+    if (process.platform === 'win32') return MEETING_PROCESSES_WIN;
+    if (process.platform === 'darwin') return MEETING_PROCESSES_MAC;
+    return MEETING_PROCESSES_LINUX;
+}

Then update checkMeetingStatus:

-            const active = MEETING_PROCESSES.some(proc => stdout.includes(proc));
+            const processes = getMeetingProcesses();
+            const active = processes.some(proc => stdout.toLowerCase().includes(proc.toLowerCase()));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/services/meeting-detector.js` around lines 5 - 11, MEETING_PROCESSES
currently lists Windows-only executable names so checkMeetingStatus never
detects meetings on macOS/Linux; update MEETING_PROCESSES into platform-aware
patterns (e.g., a map or array of case-insensitive substrings/patterns keyed by
process.platform like 'win32' => ['zoom.exe','teams.exe'], 'darwin' =>
['zoom.us','Microsoft Teams','Slack','Webex'], 'linux' =>
['zoom','teams','slack','webex']) and modify checkMeetingStatus to select the
correct pattern set based on process.platform, normalize process list output
(lowercase) and match using substring or regex matches instead of exact equality
so detection works across platforms; ensure symbols referenced are
MEETING_PROCESSES and checkMeetingStatus.


class MeetingDetector {
constructor() {
this.interval = null;
this.isMeetingActive = false;
this.mainWindow = null;
}

init(mainWindow) {
this.mainWindow = mainWindow;
this.startMonitoring();
}

startMonitoring() {
if (this.interval) return;

this.interval = setInterval(() => {
this.checkMeetingStatus();
}, 30000); // Check every 30 seconds

this.checkMeetingStatus();
}

stopMonitoring() {
if (this.interval) {
clearInterval(this.interval);
this.interval = null;
}
}

checkMeetingStatus() {
const command = process.platform === 'win32' ? 'tasklist' : 'ps -e';

exec(command, (error, stdout) => {
if (error) {
logger.error('[MeetingDetector] Failed to list processes:', error);
return;
}

const active = MEETING_PROCESSES.some(proc => stdout.includes(proc));
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Process detection may have false positives.

Using stdout.includes(proc) can match unrelated processes. For example, "ZoomIt.exe" (a screen zoom utility) would match "Zoom". Consider using word boundaries or more precise matching.

Proposed improvement
-            const active = MEETING_PROCESSES.some(proc => stdout.includes(proc));
+            const active = MEETING_PROCESSES.some(proc => {
+                // Use regex with word boundary for more precise matching
+                const pattern = new RegExp(`\\b${proc.replace('.', '\\.')}\\b`, 'i');
+                return pattern.test(stdout);
+            });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const active = MEETING_PROCESSES.some(proc => stdout.includes(proc));
const active = MEETING_PROCESSES.some(proc => {
// Use regex with word boundary for more precise matching
const pattern = new RegExp(`\\b${proc.replace('.', '\\.')}\\b`, 'i');
return pattern.test(stdout);
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/services/meeting-detector.js` at line 51, The detection using
stdout.includes(proc) in the MEETING_PROCESSES check is too permissive and can
yield false positives (e.g., "ZoomIt.exe" matching "Zoom"); update the logic
that computes active (the const active = MEETING_PROCESSES.some(...)) to perform
more precise matching by normalizing the process list and matching full process
names or using word-boundary/anchored regular expressions per entry (or
splitting stdout into lines and comparing base filenames), ensuring you
reference MEETING_PROCESSES and the active variable when making the change.


if (active && !this.isMeetingActive) {
this.onMeetingDetected();
}

this.isMeetingActive = active;
});
}

onMeetingDetected() {
logger.info('[MeetingDetector] Meeting detected!');

const notification = new Notification({
title: 'Koe - Meeting Detected',
body: 'Would you like to join the meeting to take notes and summarize?',
silent: false
});

notification.show();

notification.on('click', () => {
if (this.mainWindow && !this.mainWindow.isDestroyed()) {
const { CHANNELS } = require('../../shared/constants');
this.mainWindow.webContents.send(CHANNELS.MEETING_DETECTED);
this.mainWindow.show();
}
});
Comment on lines +70 to +78
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Notification shown before click handler registration.

notification.show() is called at Line 70 before registering the click event handler at Line 72. If a user clicks extremely fast, the handler might not be registered. Move show() after registering the handler.

Proposed fix
         const notification = new Notification({
             title: 'Koe - Meeting Detected',
             body: 'Would you like to join the meeting to take notes and summarize?',
             silent: false
         });

-        notification.show();
-
         notification.on('click', () => {
             if (this.mainWindow && !this.mainWindow.isDestroyed()) {
                 const { CHANNELS } = require('../../shared/constants');
                 this.mainWindow.webContents.send(CHANNELS.MEETING_DETECTED);
                 this.mainWindow.show();
             }
         });
+
+        notification.show();
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
notification.show();
notification.on('click', () => {
if (this.mainWindow && !this.mainWindow.isDestroyed()) {
const { CHANNELS } = require('../../shared/constants');
this.mainWindow.webContents.send(CHANNELS.MEETING_DETECTED);
this.mainWindow.show();
}
});
const notification = new Notification({
title: 'Koe - Meeting Detected',
body: 'Would you like to join the meeting to take notes and summarize?',
silent: false
});
notification.on('click', () => {
if (this.mainWindow && !this.mainWindow.isDestroyed()) {
const { CHANNELS } = require('../../shared/constants');
this.mainWindow.webContents.send(CHANNELS.MEETING_DETECTED);
this.mainWindow.show();
}
});
notification.show();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/services/meeting-detector.js` around lines 70 - 78, The notification
is shown before its 'click' handler is registered, risking missed clicks; move
the notification.show() call to after the notification.on('click', ...)
registration so the handler on the Notification instance is set before display.
In the meeting-detector.js code that creates the Notification, register
notification.on('click', () => { if (this.mainWindow &&
!this.mainWindow.isDestroyed()) { const { CHANNELS } =
require('../../shared/constants');
this.mainWindow.webContents.send(CHANNELS.MEETING_DETECTED);
this.mainWindow.show(); } }); first, then call notification.show(), ensuring the
same Notification object and preserving the require(CHANNELS.MEETING_DETECTED)
usage and this.mainWindow checks.

}
}

module.exports = new MeetingDetector();
7 changes: 7 additions & 0 deletions src/main/services/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@ const SETTINGS_MIGRATIONS = [
changes: {
cloudProcessingEnabled: false
}
},
{
id: '2026-03-20-update-default-hotkey',
mode: 'preserve',
changes: {
hotkey: 'Alt+Shift+S'
}
}
];

Expand Down
Loading