Add Power Mode keyboard shortcuts and improve session management

Power Mode Keyboard Shortcuts:
- Add hotkeyShortcut property to PowerModeConfig for storing custom shortcuts
- Implement keyboard shortcut UI in Power Mode configuration view
- Add hotkey registration system in HotkeyManager to manage Power Mode shortcuts
- Support cleanup of shortcuts when Power Mode configurations are removed
- Post notifications when Power Mode configurations change

Explicit Power Mode Activation:
- Add optional powerModeId parameter to toggleRecord and toggleMiniRecorder
- Refactor ActiveWindowService.applyConfigurationForCurrentApp to applyConfiguration
- Support direct Power Mode activation via powerModeId instead of auto-detection
- Pass powerModeId through recording flow for explicit mode selection

Session Management Improvements:
- Fix auto-restore to preserve baseline when switching Power Modes mid-recording
- Only capture baseline state on first session creation
- Prevent subsequent beginSession calls from overwriting original baseline
- Ensure auto-restore returns to settings from before recording started

UI Refinements:
- Remove redundant "Record" label from keyboard shortcut recorder
This commit is contained in:
Beingpax 2026-01-02 19:59:31 +05:45
parent 658291635b
commit 4ea8d382a4
7 changed files with 169 additions and 45 deletions

View File

@ -73,7 +73,9 @@ class HotkeyManager: ObservableObject {
private var shortcutCurrentKeyState = false
private var lastShortcutTriggerTime: Date?
private let shortcutCooldownInterval: TimeInterval = 0.5
private var registeredPowerModeIds: Set<UUID> = []
enum HotkeyOption: String, CaseIterable {
case none = "none"
case rightOption = "rightOption"
@ -162,6 +164,21 @@ class HotkeyManager: ObservableObject {
Task { @MainActor in
try? await Task.sleep(nanoseconds: 100_000_000)
self.setupHotkeyMonitoring()
self.setupPowerModeHotkeys()
}
// Observe PowerMode configuration changes
NotificationCenter.default.addObserver(
self,
selector: #selector(powerModeConfigurationsDidChange),
name: NSNotification.Name("PowerModeConfigurationsDidChange"),
object: nil
)
}
@objc private func powerModeConfigurationsDidChange() {
Task { @MainActor in
setupPowerModeHotkeys()
}
}
@ -425,3 +442,52 @@ class HotkeyManager: ObservableObject {
}
}
}
// MARK: - PowerMode Hotkey Management
extension HotkeyManager {
func setupPowerModeHotkeys() {
let powerModesWithShortcuts = Set(PowerModeManager.shared.configurations
.filter { $0.hotkeyShortcut != nil }
.map { $0.id })
let idsToRemove = registeredPowerModeIds.subtracting(powerModesWithShortcuts)
idsToRemove.forEach { id in
KeyboardShortcuts.setShortcut(nil, for: .powerMode(id: id))
KeyboardShortcuts.disable(.powerMode(id: id))
registeredPowerModeIds.remove(id)
}
PowerModeManager.shared.configurations.forEach { config in
guard config.hotkeyShortcut != nil else { return }
guard !registeredPowerModeIds.contains(config.id) else { return }
KeyboardShortcuts.onKeyUp(for: .powerMode(id: config.id)) { [weak self] in
guard let self = self else { return }
Task { @MainActor in
await self.handlePowerModeHotkey(powerModeId: config.id)
}
}
registeredPowerModeIds.insert(config.id)
}
}
private func handlePowerModeHotkey(powerModeId: UUID) async {
guard canProcessHotkeyAction else { return }
guard let config = PowerModeManager.shared.getConfiguration(with: powerModeId),
config.hotkeyShortcut != nil else {
return
}
await whisperState.toggleMiniRecorder(powerModeId: powerModeId)
}
}
// MARK: - PowerMode Keyboard Shortcut Names
extension KeyboardShortcuts.Name {
static func powerMode(id: UUID) -> Self {
Self("powerMode_\(id.uuidString)")
}
}

View File

@ -24,7 +24,16 @@ class ActiveWindowService: ObservableObject {
self.whisperState = whisperState
}
func applyConfigurationForCurrentApp() async {
func applyConfiguration(powerModeId: UUID? = nil) async {
if let powerModeId = powerModeId,
let config = PowerModeManager.shared.getConfiguration(with: powerModeId) {
await MainActor.run {
PowerModeManager.shared.setActiveConfiguration(config)
}
await PowerModeSessionManager.shared.beginSession(with: config)
return
}
guard let frontmostApp = NSWorkspace.shared.frontmostApplication,
let bundleIdentifier = frontmostApp.bundleIdentifier else {
return
@ -60,8 +69,6 @@ class ActiveWindowService: ObservableObject {
PowerModeManager.shared.setActiveConfiguration(config)
}
await PowerModeSessionManager.shared.beginSession(with: config)
} else {
// If no config found, keep the current active configuration (don't clear it)
}
}
}

View File

@ -1,4 +1,5 @@
import Foundation
import KeyboardShortcuts
struct PowerModeConfig: Codable, Identifiable, Equatable {
var id: UUID
@ -16,9 +17,10 @@ struct PowerModeConfig: Codable, Identifiable, Equatable {
var isAutoSendEnabled: Bool = false
var isEnabled: Bool = true
var isDefault: Bool = false
var hotkeyShortcut: String? = nil
enum CodingKeys: String, CodingKey {
case id, name, emoji, appConfigs, urlConfigs, isAIEnhancementEnabled, selectedPrompt, selectedLanguage, useScreenCapture, selectedAIProvider, selectedAIModel, isAutoSendEnabled, isEnabled, isDefault
case id, name, emoji, appConfigs, urlConfigs, isAIEnhancementEnabled, selectedPrompt, selectedLanguage, useScreenCapture, selectedAIProvider, selectedAIModel, isAutoSendEnabled, isEnabled, isDefault, hotkeyShortcut
case selectedWhisperModel
case selectedTranscriptionModelName
}
@ -26,7 +28,7 @@ struct PowerModeConfig: Codable, Identifiable, Equatable {
init(id: UUID = UUID(), name: String, emoji: String, appConfigs: [AppConfig]? = nil,
urlConfigs: [URLConfig]? = nil, isAIEnhancementEnabled: Bool, selectedPrompt: String? = nil,
selectedTranscriptionModelName: String? = nil, selectedLanguage: String? = nil, useScreenCapture: Bool = false,
selectedAIProvider: String? = nil, selectedAIModel: String? = nil, isAutoSendEnabled: Bool = false, isEnabled: Bool = true, isDefault: Bool = false) {
selectedAIProvider: String? = nil, selectedAIModel: String? = nil, isAutoSendEnabled: Bool = false, isEnabled: Bool = true, isDefault: Bool = false, hotkeyShortcut: String? = nil) {
self.id = id
self.name = name
self.emoji = emoji
@ -42,6 +44,7 @@ struct PowerModeConfig: Codable, Identifiable, Equatable {
self.selectedLanguage = selectedLanguage ?? UserDefaults.standard.string(forKey: "SelectedLanguage") ?? "en"
self.isEnabled = isEnabled
self.isDefault = isDefault
self.hotkeyShortcut = hotkeyShortcut
}
init(from decoder: Decoder) throws {
@ -60,6 +63,7 @@ struct PowerModeConfig: Codable, Identifiable, Equatable {
isAutoSendEnabled = try container.decodeIfPresent(Bool.self, forKey: .isAutoSendEnabled) ?? false
isEnabled = try container.decodeIfPresent(Bool.self, forKey: .isEnabled) ?? true
isDefault = try container.decodeIfPresent(Bool.self, forKey: .isDefault) ?? false
hotkeyShortcut = try container.decodeIfPresent(String.self, forKey: .hotkeyShortcut)
if let newModelName = try container.decodeIfPresent(String.self, forKey: .selectedTranscriptionModelName) {
selectedTranscriptionModelName = newModelName
@ -87,6 +91,7 @@ struct PowerModeConfig: Codable, Identifiable, Equatable {
try container.encodeIfPresent(selectedTranscriptionModelName, forKey: .selectedTranscriptionModelName)
try container.encode(isEnabled, forKey: .isEnabled)
try container.encode(isDefault, forKey: .isDefault)
try container.encodeIfPresent(hotkeyShortcut, forKey: .hotkeyShortcut)
}
@ -155,6 +160,7 @@ class PowerModeManager: ObservableObject {
if let data = try? JSONEncoder().encode(configurations) {
UserDefaults.standard.set(data, forKey: configKey)
}
NotificationCenter.default.post(name: NSNotification.Name("PowerModeConfigurationsDidChange"), object: nil)
}
func addConfiguration(_ config: PowerModeConfig) {
@ -165,6 +171,7 @@ class PowerModeManager: ObservableObject {
}
func removeConfiguration(with id: UUID) {
KeyboardShortcuts.setShortcut(nil, for: .powerMode(id: id))
configurations.removeAll { $0.id == id }
saveConfigurations()
}
@ -221,18 +228,18 @@ class PowerModeManager: ObservableObject {
return configurations.contains { $0.isDefault }
}
func setAsDefault(configId: UUID) {
// Clear any existing default
func setAsDefault(configId: UUID, skipSave: Bool = false) {
for index in configurations.indices {
configurations[index].isDefault = false
}
// Set the specified config as default
if let index = configurations.firstIndex(where: { $0.id == configId }) {
configurations[index].isDefault = true
}
saveConfigurations()
if !skipSave {
saveConfigurations()
}
}
func enableConfiguration(with id: UUID) {

View File

@ -1,4 +1,5 @@
import SwiftUI
import KeyboardShortcuts
struct ConfigurationView: View {
let mode: ConfigurationMode
@ -42,6 +43,9 @@ struct ConfigurationView: View {
@State private var isEditingPrompt = false
@State private var selectedPromptForEdit: CustomPrompt?
// PowerMode hotkey configuration
@State private var powerModeConfigId: UUID = UUID()
private func languageSelectionDisabled() -> Bool {
guard let selectedModelName = effectiveModelName,
let model = whisperState.allAvailableModels.first(where: { $0.name == selectedModelName })
@ -83,10 +87,12 @@ struct ConfigurationView: View {
init(mode: ConfigurationMode, powerModeManager: PowerModeManager) {
self.mode = mode
self.powerModeManager = powerModeManager
// Always fetch the most current configuration data
switch mode {
case .add:
let newId = UUID()
_powerModeConfigId = State(initialValue: newId)
_isAIEnhancementEnabled = State(initialValue: true)
_selectedPromptId = State(initialValue: nil)
_selectedTranscriptionModelName = State(initialValue: nil)
@ -102,6 +108,7 @@ struct ConfigurationView: View {
case .edit(let config):
// Get the latest version of this config from PowerModeManager
let latestConfig = powerModeManager.getConfiguration(with: config.id) ?? config
_powerModeConfigId = State(initialValue: latestConfig.id)
_isAIEnhancementEnabled = State(initialValue: latestConfig.isAIEnhancementEnabled)
_selectedPromptId = State(initialValue: latestConfig.selectedPrompt.flatMap { UUID(uuidString: $0) })
_selectedTranscriptionModelName = State(initialValue: latestConfig.selectedTranscriptionModelName)
@ -613,12 +620,30 @@ struct ConfigurationView: View {
HStack {
Toggle("Auto Send", isOn: $isAutoSendEnabled)
InfoTip(
title: "Auto Send",
message: "Automatically presses the Return/Enter key after pasting text. This is useful for chat applications or forms where its not necessary to to make changes to the transcribed text"
)
Spacer()
}
Divider()
HStack {
Text("Keyboard Shortcut")
.font(.subheadline)
.foregroundColor(.secondary)
KeyboardShortcuts.Recorder(for: .powerMode(id: powerModeConfigId))
.controlSize(.small)
InfoTip(
title: "Power Mode Hotkey",
message: "Assign a unique keyboard shortcut to instantly activate this Power Mode and start recording"
)
Spacer()
}
}
@ -705,9 +730,13 @@ struct ConfigurationView: View {
}
private func getConfigForForm() -> PowerModeConfig {
let shortcut = KeyboardShortcuts.getShortcut(for: .powerMode(id: powerModeConfigId))
let hotkeyString = shortcut != nil ? "configured" : nil
switch mode {
case .add:
return PowerModeConfig(
id: powerModeConfigId,
name: configName,
emoji: selectedEmoji,
appConfigs: selectedAppConfigs.isEmpty ? nil : selectedAppConfigs,
@ -720,7 +749,8 @@ struct ConfigurationView: View {
selectedAIProvider: selectedAIProvider,
selectedAIModel: selectedAIModel,
isAutoSendEnabled: isAutoSendEnabled,
isDefault: isDefault
isDefault: isDefault,
hotkeyShortcut: hotkeyString
)
case .edit(let config):
var updatedConfig = config
@ -737,6 +767,7 @@ struct ConfigurationView: View {
updatedConfig.selectedAIProvider = selectedAIProvider
updatedConfig.selectedAIModel = selectedAIModel
updatedConfig.isDefault = isDefault
updatedConfig.hotkeyShortcut = hotkeyString
return updatedConfig
}
}
@ -817,19 +848,17 @@ struct ConfigurationView: View {
return
}
// If validation passes, save the configuration
if isDefault {
powerModeManager.setAsDefault(configId: config.id, skipSave: true)
}
switch mode {
case .add:
powerModeManager.addConfiguration(config)
case .edit:
powerModeManager.updateConfiguration(config)
}
// Handle default flag separately to ensure only one config is default
if isDefault {
powerModeManager.setAsDefault(configId: config.id)
}
presentationMode.wrappedValue.dismiss()
}
}

View File

@ -41,30 +41,38 @@ class PowerModeSessionManager {
return
}
let originalState = ApplicationState(
isEnhancementEnabled: enhancementService.isEnhancementEnabled,
useScreenCaptureContext: enhancementService.useScreenCaptureContext,
selectedPromptId: enhancementService.selectedPromptId?.uuidString,
selectedAIProvider: enhancementService.getAIService()?.selectedProvider.rawValue,
selectedAIModel: enhancementService.getAIService()?.currentModel,
selectedLanguage: UserDefaults.standard.string(forKey: "SelectedLanguage"),
transcriptionModelName: whisperState.currentTranscriptionModel?.name
)
// Only capture baseline if NO session exists
if loadSession() == nil {
let originalState = ApplicationState(
isEnhancementEnabled: enhancementService.isEnhancementEnabled,
useScreenCaptureContext: enhancementService.useScreenCaptureContext,
selectedPromptId: enhancementService.selectedPromptId?.uuidString,
selectedAIProvider: enhancementService.getAIService()?.selectedProvider.rawValue,
selectedAIModel: enhancementService.getAIService()?.currentModel,
selectedLanguage: UserDefaults.standard.string(forKey: "SelectedLanguage"),
transcriptionModelName: whisperState.currentTranscriptionModel?.name
)
let newSession = PowerModeSession(
id: UUID(),
startTime: Date(),
originalState: originalState
)
saveSession(newSession)
NotificationCenter.default.addObserver(self, selector: #selector(updateSessionSnapshot), name: .AppSettingsDidChange, object: nil)
let newSession = PowerModeSession(
id: UUID(),
startTime: Date(),
originalState: originalState
)
saveSession(newSession)
NotificationCenter.default.addObserver(self, selector: #selector(updateSessionSnapshot), name: .AppSettingsDidChange, object: nil)
}
// Always apply the new configuration
isApplyingPowerModeConfig = true
await applyConfiguration(config)
isApplyingPowerModeConfig = false
}
var hasActiveSession: Bool {
return loadSession() != nil
}
func endSession() async {
guard let session = loadSession() else { return }

View File

@ -32,10 +32,10 @@ extension WhisperState {
// MARK: - Mini Recorder Management
func toggleMiniRecorder() async {
func toggleMiniRecorder(powerModeId: UUID? = nil) async {
if isMiniRecorderVisible {
if recordingState == .recording {
await toggleRecord()
await toggleRecord(powerModeId: powerModeId)
} else {
await cancelRecording()
}
@ -46,7 +46,7 @@ extension WhisperState {
isMiniRecorderVisible = true // This will call showRecorderPanel() via didSet
}
await toggleRecord()
await toggleRecord(powerModeId: powerModeId)
}
}

View File

@ -138,7 +138,7 @@ class WhisperState: NSObject, ObservableObject {
}
}
func toggleRecord() async {
func toggleRecord(powerModeId: UUID? = nil) async {
if recordingState == .recording {
await recorder.stopRecording()
if let recordedFile {
@ -195,7 +195,14 @@ class WhisperState: NSObject, ObservableObject {
self.recordingState = .recording
}
await ActiveWindowService.shared.applyConfigurationForCurrentApp()
if let powerModeId = powerModeId {
await ActiveWindowService.shared.applyConfiguration(powerModeId: powerModeId)
} else {
let hasActiveSession = await PowerModeSessionManager.shared.hasActiveSession
if !hasActiveSession {
await ActiveWindowService.shared.applyConfiguration()
}
}
// Load model and capture context in background without blocking
Task.detached { [weak self] in