Merge pull request #462 from Beingpax/powermode-enhancements

Add Power Mode keyboard shortcuts and improve session management
This commit is contained in:
Prakash Joshi Pax 2026-01-03 11:43:57 +05:45 committed by GitHub
commit c7fe067252
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 189 additions and 45 deletions

View File

@ -43,6 +43,7 @@ class HotkeyManager: ObservableObject {
private var whisperState: WhisperState
private var miniRecorderShortcutManager: MiniRecorderShortcutManager
private var powerModeShortcutManager: PowerModeShortcutManager
// MARK: - Helper Properties
private var canProcessHotkeyAction: Bool {
@ -73,7 +74,7 @@ class HotkeyManager: ObservableObject {
private var shortcutCurrentKeyState = false
private var lastShortcutTriggerTime: Date?
private let shortcutCooldownInterval: TimeInterval = 0.5
enum HotkeyOption: String, CaseIterable {
case none = "none"
case rightOption = "rightOption"
@ -127,6 +128,7 @@ class HotkeyManager: ObservableObject {
self.whisperState = whisperState
self.miniRecorderShortcutManager = MiniRecorderShortcutManager(whisperState: whisperState)
self.powerModeShortcutManager = PowerModeShortcutManager(whisperState: whisperState)
KeyboardShortcuts.onKeyUp(for: .pasteLastTranscription) { [weak self] in
guard let self = self else { return }

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

@ -0,0 +1,84 @@
import Foundation
import KeyboardShortcuts
@MainActor
class PowerModeShortcutManager {
private weak var whisperState: WhisperState?
private var registeredPowerModeIds: Set<UUID> = []
init(whisperState: WhisperState) {
self.whisperState = whisperState
setupPowerModeHotkeys()
NotificationCenter.default.addObserver(
self,
selector: #selector(powerModeConfigurationsDidChange),
name: NSNotification.Name("PowerModeConfigurationsDidChange"),
object: nil
)
}
deinit {
NotificationCenter.default.removeObserver(self)
}
@objc private func powerModeConfigurationsDidChange() {
Task { @MainActor in
setupPowerModeHotkeys()
}
}
private func setupPowerModeHotkeys() {
let powerModesWithShortcuts = Set(PowerModeManager.shared.configurations
.filter { $0.hotkeyShortcut != nil }
.map { $0.id })
// Remove shortcuts for deleted or updated configs
let idsToRemove = registeredPowerModeIds.subtracting(powerModesWithShortcuts)
idsToRemove.forEach { id in
KeyboardShortcuts.setShortcut(nil, for: .powerMode(id: id))
registeredPowerModeIds.remove(id)
}
// Add new shortcuts
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 let whisperState = whisperState,
canProcessHotkeyAction(whisperState: whisperState) else { return }
guard let config = PowerModeManager.shared.getConfiguration(with: powerModeId),
config.hotkeyShortcut != nil else {
return
}
await whisperState.toggleMiniRecorder(powerModeId: powerModeId)
}
private func canProcessHotkeyAction(whisperState: WhisperState) -> Bool {
whisperState.recordingState != .transcribing &&
whisperState.recordingState != .enhancing &&
whisperState.recordingState != .busy
}
}
// MARK: - PowerMode Keyboard Shortcut Names
extension KeyboardShortcuts.Name {
static func powerMode(id: UUID) -> Self {
Self("powerMode_\(id.uuidString)")
}
}

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