Skip to content
Closed
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
29 changes: 29 additions & 0 deletions main.js
Original file line number Diff line number Diff line change
Expand Up @@ -612,6 +612,35 @@ async function startApp() {
}
});

// Set up meeting mode hotkey
const meetingHotkeyCallback = () => {
if (hotkeyManager.isInListeningMode()) return;
if (meetingDetectionEngine?.isInManualMeetingMode()) {
debugLogger.info("Meeting hotkey pressed, ending manual meeting mode", {}, "meeting");
meetingDetectionEngine.endManualMeetingMode();
} else {
debugLogger.info("Meeting hotkey pressed, triggering force meeting mode", {}, "meeting");
meetingDetectionEngine?.forceMeetingMode();
}
};

const savedMeetingKey = environmentManager.getMeetingKey?.() || "Control+Shift+M";
debugLogger.info("Registering meeting hotkey", { hotkey: savedMeetingKey }, "meeting");
const meetingResult = hotkeyManager.registerSlot("meeting", savedMeetingKey, meetingHotkeyCallback);
debugLogger.info("Meeting hotkey registration result", { success: meetingResult.success, hotkey: savedMeetingKey }, "meeting");

ipcMain.on("meeting-hotkey-changed", (_event, hotkey) => {
debugLogger.info("Meeting hotkey changed", { hotkey }, "meeting");
if (hotkey) {
const result = hotkeyManager.registerSlot("meeting", hotkey, meetingHotkeyCallback);
debugLogger.info("Meeting hotkey re-registration result", { success: result.success, hotkey }, "meeting");
environmentManager.saveMeetingKey(hotkey);
} else {
hotkeyManager.unregisterSlot("meeting");
environmentManager.saveMeetingKey("");
}
});

// Phase 2: Initialize remaining managers after windows are visible
initializeDeferredManagers();

Expand Down
3 changes: 3 additions & 0 deletions preload.js
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,7 @@ contextBridge.exposeInMainWorld("electronAPI", {
// Notify main process of activation mode changes (for Windows Push-to-Talk)
notifyActivationModeChanged: (mode) => ipcRenderer.send("activation-mode-changed", mode),
notifyHotkeyChanged: (hotkey) => ipcRenderer.send("hotkey-changed", hotkey),
notifyMeetingHotkeyChanged: (hotkey) => ipcRenderer.send("meeting-hotkey-changed", hotkey),

// Floating icon auto-hide
notifyFloatingIconAutoHideChanged: (enabled) =>
Expand Down Expand Up @@ -649,6 +650,8 @@ contextBridge.exposeInMainWorld("electronAPI", {
meetingNotificationReady: () => ipcRenderer.invoke("meeting-notification-ready"),
meetingNotificationRespond: (detectionId, action) =>
ipcRenderer.invoke("meeting-notification-respond", detectionId, action),
forceMeetingMode: () => ipcRenderer.invoke("force-meeting-mode"),
endManualMeetingMode: () => ipcRenderer.invoke("end-manual-meeting-mode"),
onNavigateToMeetingNote: registerListener(
"navigate-to-meeting-note",
(callback) => (_event, data) => callback(data)
Expand Down
32 changes: 32 additions & 0 deletions src/components/SettingsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -681,6 +681,8 @@ export default function SettingsPage({ activeSection = "general" }: SettingsPage
customReasoningApiKey,
setCustomReasoningApiKey,
setDictationKey,
meetingKey,
setMeetingKey,
autoLearnCorrections,
setAutoLearnCorrections,
updateTranscriptionSettings,
Expand Down Expand Up @@ -858,6 +860,16 @@ export default function SettingsPage({ activeSection = "general" }: SettingsPage
showAlert: showAlertDialog,
});

const { registerHotkey: registerMeetingHotkey, isRegistering: isMeetingHotkeyRegistering } =
useHotkeyRegistration({
onSuccess: (registeredHotkey) => {
setMeetingKey(registeredHotkey);
},
showSuccessToast: false,
showErrorToast: true,
showAlert: showAlertDialog,
});

const validateHotkeyForInput = useCallback(
(hotkey: string) => getValidationMessage(hotkey, getPlatform()),
[]
Expand Down Expand Up @@ -2642,6 +2654,26 @@ EOF`,
)}
</SettingsPanel>
</div>

{/* Meeting Mode Hotkey */}
<div>
<SectionHeader
title={t("settingsPage.general.meetingHotkey.title")}
description={t("settingsPage.general.meetingHotkey.description")}
/>
<SettingsPanel>
<SettingsPanelRow>
<HotkeyInput
value={meetingKey}
onChange={async (newHotkey) => {
await registerMeetingHotkey(newHotkey);
}}
disabled={isMeetingHotkeyRegistering}
validate={validateHotkeyForInput}
/>
</SettingsPanelRow>
</SettingsPanel>
</div>
</div>
);

Expand Down
11 changes: 11 additions & 0 deletions src/helpers/environment.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const PERSISTED_KEYS = [
"LLAMA_VULKAN_ENABLED",
"DICTATION_KEY",
"AGENT_KEY",
"MEETING_KEY",
"ACTIVATION_MODE",
"FLOATING_ICON_AUTO_HIDE",
"START_MINIMIZED",
Expand Down Expand Up @@ -143,6 +144,16 @@ class EnvironmentManager {
return result;
}

getMeetingKey() {
return this._getKey("MEETING_KEY");
}

saveMeetingKey(key) {
const result = this._saveKey("MEETING_KEY", key);
this.saveAllKeysToEnvFile().catch(() => {});
return result;
}

getActivationMode() {
const mode = this._getKey("ACTIVATION_MODE");
return mode === "push" ? "push" : "tap";
Expand Down
18 changes: 18 additions & 0 deletions src/helpers/ipcHandlers.js
Original file line number Diff line number Diff line change
Expand Up @@ -3915,6 +3915,24 @@ class IPCHandlers {
this.windowManager?.showNotificationWindow();
});

ipcMain.handle("force-meeting-mode", async () => {
try {
await this.meetingDetectionEngine.forceMeetingMode();
return { success: true };
} catch (error) {
return { success: false, error: error.message };
}
});

ipcMain.handle("end-manual-meeting-mode", async () => {
try {
this.meetingDetectionEngine.endManualMeetingMode();
return { success: true };
} catch (error) {
return { success: false, error: error.message };
}
});

ipcMain.handle("get-desktop-sources", async (_event, types) => {
try {
const { desktopCapturer } = require("electron");
Expand Down
58 changes: 58 additions & 0 deletions src/helpers/meetingDetectionEngine.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ class MeetingDetectionEngine {
this.activeDetections = new Map();
this.preferences = { processDetection: true, audioDetection: true };
this._userRecording = false;
this._manualMeetingMode = false;
this._notificationQueue = [];
this._postRecordingCooldown = null;
this._bindListeners();
Expand Down Expand Up @@ -55,6 +56,15 @@ class MeetingDetectionEngine {
return;
}

if (this._manualMeetingMode) {
debugLogger.info(
"Suppressing detection — manual meeting mode active",
{ detectionId },
"meeting"
);
return;
}

const calendarState = this.googleCalendarManager?.getActiveMeetingState?.();
if (calendarState) {
if (calendarState.activeMeeting) {
Expand Down Expand Up @@ -199,6 +209,53 @@ class MeetingDetectionEngine {
}
}

async forceMeetingMode() {
if (this._manualMeetingMode) {
debugLogger.info("Already in manual meeting mode, ignoring", {}, "meeting");
return;
}

debugLogger.info("Force meeting mode triggered", {}, "meeting");
this._manualMeetingMode = true;

const event = {
id: `manual-${Date.now()}`,
calendar_id: "__manual__",
summary: "New note",
start_time: new Date().toISOString(),
end_time: new Date(Date.now() + 3600000).toISOString(),
is_all_day: 0,
status: "confirmed",
hangout_link: null,
conference_data: null,
organizer_email: null,
attendees_count: 0,
};

const noteResult = this.databaseManager.saveNote(event.summary, "", "meeting");
const meetingsFolder = this.databaseManager.getMeetingsFolder();

if (noteResult?.note?.id && meetingsFolder?.id) {
await this.windowManager.createControlPanelWindow();
this.windowManager.snapControlPanelToMeetingMode();
this.windowManager.sendToControlPanel("navigate-to-meeting-note", {
noteId: noteResult.note.id,
folderId: meetingsFolder.id,
event,
});
}
}

endManualMeetingMode() {
if (!this._manualMeetingMode) return;
debugLogger.info("Manual meeting mode ended", {}, "meeting");
this._manualMeetingMode = false;
}

isInManualMeetingMode() {
return this._manualMeetingMode;
}

handleNotificationTimeout() {
for (const [detectionId, detection] of this.activeDetections) {
if (!detection.dismissed) {
Expand Down Expand Up @@ -306,6 +363,7 @@ class MeetingDetectionEngine {
this.meetingProcessDetector.stop();
this.audioActivityDetector.stop();
this.activeDetections.clear();
this._manualMeetingMode = false;
if (this._postRecordingCooldown) {
clearTimeout(this._postRecordingCooldown);
this._postRecordingCooldown = null;
Expand Down
3 changes: 3 additions & 0 deletions src/hooks/useSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export interface ReasoningSettings {

export interface HotkeySettings {
dictationKey: string;
meetingKey: string;
activationMode: "tap" | "push";
}

Expand Down Expand Up @@ -188,6 +189,7 @@ function useSettingsInternal() {
groqApiKey: store.groqApiKey,
mistralApiKey: store.mistralApiKey,
dictationKey: store.dictationKey,
meetingKey: store.meetingKey,
theme: store.theme,
setUseLocalWhisper: store.setUseLocalWhisper,
setWhisperModel: store.setWhisperModel,
Expand Down Expand Up @@ -218,6 +220,7 @@ function useSettingsInternal() {
customReasoningApiKey: store.customReasoningApiKey,
setCustomReasoningApiKey: store.setCustomReasoningApiKey,
setDictationKey: store.setDictationKey,
setMeetingKey: store.setMeetingKey,
setTheme: store.setTheme,
activationMode: store.activationMode,
setActivationMode: store.setActivationMode,
Expand Down
4 changes: 4 additions & 0 deletions src/locales/de/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -1130,6 +1130,10 @@
"resetToDefault": "Auf {{hotkey}} zurücksetzen",
"title": "Diktat-Hotkey"
},
"meetingHotkey": {
"title": "Meeting-Modus-Hotkey",
"description": "Meeting-Modus manuell starten und automatische Benachrichtigungen unterdrücken"
},
"microphone": {
"description": "Wählen Sie das Eingabegerät für das Diktat",
"title": "Mikrofon"
Expand Down
4 changes: 4 additions & 0 deletions src/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -1190,6 +1190,10 @@
"resetToDefault": "Reset to {{hotkey}}",
"title": "Dictation Hotkey"
},
"meetingHotkey": {
"title": "Meeting Mode Hotkey",
"description": "Manually start meeting mode and suppress automatic meeting notifications"
},
"microphone": {
"description": "Select which input device to use for dictation",
"title": "Microphone"
Expand Down
4 changes: 4 additions & 0 deletions src/locales/es/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -1130,6 +1130,10 @@
"resetToDefault": "Restablecer a {{hotkey}}",
"title": "Atajo de dictado"
},
"meetingHotkey": {
"title": "Hotkey modo reunión",
"description": "Iniciar manualmente el modo reunión y suprimir las notificaciones automáticas"
},
"microphone": {
"description": "Selecciona qué dispositivo de entrada usar para dictado",
"title": "Micrófono"
Expand Down
4 changes: 4 additions & 0 deletions src/locales/fr/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -1130,6 +1130,10 @@
"resetToDefault": "Réinitialiser à {{hotkey}}",
"title": "Raccourci de dictée"
},
"meetingHotkey": {
"title": "Raccourci mode réunion",
"description": "Démarrer manuellement le mode réunion et supprimer les notifications automatiques"
},
"microphone": {
"description": "Sélectionnez le périphérique d'entrée à utiliser pour la dictée",
"title": "Microphone"
Expand Down
4 changes: 4 additions & 0 deletions src/locales/it/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -1130,6 +1130,10 @@
"resetToDefault": "Ripristina a {{hotkey}}",
"title": "Scorciatoia di dettatura"
},
"meetingHotkey": {
"title": "Hotkey modalità meeting",
"description": "Avvia manualmente la modalità meeting e sopprimi le notifiche automatiche"
},
"microphone": {
"description": "Seleziona il dispositivo di ingresso da usare per la dettatura",
"title": "Microfono"
Expand Down
4 changes: 4 additions & 0 deletions src/locales/ja/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -1130,6 +1130,10 @@
"resetToDefault": "{{hotkey}}にリセット",
"title": "ディクテーションホットキー"
},
"meetingHotkey": {
"title": "ミーティングモードホットキー",
"description": "ミーティングモードを手動で開始し、自動通知を抑制する"
},
"microphone": {
"description": "ディクテーションに使用する入力デバイスを選択",
"title": "マイク"
Expand Down
4 changes: 4 additions & 0 deletions src/locales/pt/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -1102,6 +1102,10 @@
"resetToDefault": "Redefinir para {{hotkey}}",
"title": "Atalho de ditado"
},
"meetingHotkey": {
"title": "Hotkey modo reunião",
"description": "Iniciar manualmente o modo reunião e suprimir notificações automáticas"
},
"microphone": {
"description": "Selecione qual dispositivo de entrada usar para ditado",
"title": "Microfone"
Expand Down
4 changes: 4 additions & 0 deletions src/locales/ru/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -1130,6 +1130,10 @@
"resetToDefault": "Сбросить на {{hotkey}}",
"title": "Горячая клавиша диктовки"
},
"meetingHotkey": {
"title": "Горячая клавиша режима встречи",
"description": "Вручную запустить режим встречи и подавить автоматические уведомления"
},
"microphone": {
"description": "Выберите устройство ввода для диктовки",
"title": "Микрофон"
Expand Down
4 changes: 4 additions & 0 deletions src/locales/zh-CN/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -1130,6 +1130,10 @@
"resetToDefault": "重置为 {{hotkey}}",
"title": "听写快捷键"
},
"meetingHotkey": {
"title": "会议模式快捷键",
"description": "手动启动会议模式并抑制自动通知"
},
"microphone": {
"description": "选择用于听写的输入设备",
"title": "麦克风"
Expand Down
4 changes: 4 additions & 0 deletions src/locales/zh-TW/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -1130,6 +1130,10 @@
"resetToDefault": "重置為 {{hotkey}}",
"title": "語音輸入快捷鍵"
},
"meetingHotkey": {
"title": "會議模式快捷鍵",
"description": "手動啟動會議模式並抑制自動通知"
},
"microphone": {
"description": "選擇用於語音輸入的輸入裝置",
"title": "麥克風"
Expand Down
Loading
Loading