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
72 changes: 50 additions & 22 deletions FreeThinker/Core/Models/AppSettings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,16 @@ public enum ProvocationStylePreset: String, Codable, CaseIterable, Identifiable,
public var instruction: String {
switch self {
case .contrarian:
return "Take a rigorous contrary angle. Surface weak premises and overconfidence. What might be wrong, incomplete, or deserving of skepticism? What counterarguments could be made?"
return "Take a rigorous contrary angle. Surface weak premises and overconfidence. "
+ "What might be wrong, incomplete, or deserving of skepticism? What counterarguments could be made?"
case .socratic:
return "Use Socratic questioning to challenge assumptions and reveal gaps in reasoning. Challenge the main assumptions or claims made here. What might be wrong, incomplete, or deserving of skepticism?"
return "Use Socratic questioning to challenge assumptions and reveal gaps in reasoning. "
+ "Challenge the main assumptions or claims made here. "
+ "What might be wrong, incomplete, or deserving of skepticism?"
case .systemsThinking:
return "Analyze second-order effects, feedback loops, and systemic tradeoffs. What are the broader implications? What connections can you draw to other fields or concepts? How might this extend or apply in unexpected ways?"
return "Analyze second-order effects, feedback loops, and systemic tradeoffs. "
+ "What are the broader implications? What connections can you draw to other fields or concepts? "
+ "How might this extend or apply in unexpected ways?"
}
}
}
Expand Down Expand Up @@ -240,7 +245,8 @@ private extension HotkeyDisplayFormatter {
41: ";", 42: "\\", 43: ",", 44: "/", 45: "N", 46: "M", 47: ".", 48: "Tab", 49: "Space", 50: "`",
51: "Delete", 53: "Esc",
122: "F1", 120: "F2", 99: "F3", 118: "F4", 96: "F5", 97: "F6", 98: "F7", 100: "F8", 101: "F9", 109: "F10",
103: "F11", 111: "F12", 105: "F13", 107: "F14", 113: "F15", 106: "F16", 64: "F17", 79: "F18", 80: "F19", 90: "F20",
103: "F11", 111: "F12", 105: "F13", 107: "F14", 113: "F15",
106: "F16", 64: "F17", 79: "F18", 80: "F19", 90: "F20",
123: "Left", 124: "Right", 125: "Down", 126: "Up"
]

Expand Down Expand Up @@ -296,9 +302,11 @@ public struct AppSettings: Codable, Equatable, Sendable {
/// Default key code for the global hotkey (P = 35).
public static let defaultHotkeyKeyCode = 35
/// Default text for the first custom prompt template.
public static let defaultPrompt1 = "Identify hidden assumptions, unstated premises, or implicit biases in the following text."
public static let defaultPrompt1 =
"Identify hidden assumptions, unstated premises, or implicit biases in the following text."
/// Default text for the second custom prompt template.
public static let defaultPrompt2 = "Provide a strong, well-reasoned counterargument or alternative perspective to the following claim."
public static let defaultPrompt2 =
"Provide a strong, well-reasoned counterargument or alternative perspective to the following claim."
/// Maximum allowed length (in characters) for prompt1 and prompt2.
public static let maxPromptLength = 1_000
/// Maximum allowed length (in characters) for `customStyleInstructions`.
Expand Down Expand Up @@ -446,20 +454,36 @@ public extension AppSettings {
prompt1 = try container.decodeIfPresent(String.self, forKey: .prompt1) ?? defaults.prompt1
prompt2 = try container.decodeIfPresent(String.self, forKey: .prompt2) ?? defaults.prompt2
launchAtLogin = try container.decodeIfPresent(Bool.self, forKey: .launchAtLogin) ?? defaults.launchAtLogin
selectedModel = try container.decodeIfPresent(ModelOption.self, forKey: .selectedModel) ?? defaults.selectedModel
showMenuBarIcon = try container.decodeIfPresent(Bool.self, forKey: .showMenuBarIcon) ?? defaults.showMenuBarIcon
dismissOnCopy = try container.decodeIfPresent(Bool.self, forKey: .dismissOnCopy) ?? defaults.dismissOnCopy
autoDismissSeconds = try container.decodeIfPresent(TimeInterval.self, forKey: .autoDismissSeconds) ?? defaults.autoDismissSeconds
fallbackCaptureEnabled = try container.decodeIfPresent(Bool.self, forKey: .fallbackCaptureEnabled) ?? defaults.fallbackCaptureEnabled
diagnosticsEnabled = try container.decodeIfPresent(Bool.self, forKey: .diagnosticsEnabled) ?? defaults.diagnosticsEnabled
hasSeenOnboarding = try container.decodeIfPresent(Bool.self, forKey: .hasSeenOnboarding) ?? defaults.hasSeenOnboarding
onboardingCompleted = try container.decodeIfPresent(Bool.self, forKey: .onboardingCompleted) ?? defaults.onboardingCompleted
hotkeyAwarenessConfirmed = try container.decodeIfPresent(Bool.self, forKey: .hotkeyAwarenessConfirmed) ?? defaults.hotkeyAwarenessConfirmed
provocationStylePreset = try container.decodeIfPresent(ProvocationStylePreset.self, forKey: .provocationStylePreset) ?? defaults.provocationStylePreset
customStyleInstructions = try container.decodeIfPresent(String.self, forKey: .customStyleInstructions) ?? defaults.customStyleInstructions
automaticallyCheckForUpdates = try container.decodeIfPresent(Bool.self, forKey: .automaticallyCheckForUpdates) ?? defaults.automaticallyCheckForUpdates
appUpdateChannel = try container.decodeIfPresent(AppUpdateChannel.self, forKey: .appUpdateChannel) ?? defaults.appUpdateChannel
aiTimeoutSeconds = try container.decodeIfPresent(TimeInterval.self, forKey: .aiTimeoutSeconds) ?? defaults.aiTimeoutSeconds
selectedModel = try container.decodeIfPresent(ModelOption.self, forKey: .selectedModel)
?? defaults.selectedModel
showMenuBarIcon = try container.decodeIfPresent(Bool.self, forKey: .showMenuBarIcon)
?? defaults.showMenuBarIcon
dismissOnCopy = try container.decodeIfPresent(Bool.self, forKey: .dismissOnCopy)
?? defaults.dismissOnCopy
autoDismissSeconds = try container.decodeIfPresent(TimeInterval.self, forKey: .autoDismissSeconds)
?? defaults.autoDismissSeconds
fallbackCaptureEnabled = try container.decodeIfPresent(Bool.self, forKey: .fallbackCaptureEnabled)
?? defaults.fallbackCaptureEnabled
diagnosticsEnabled = try container.decodeIfPresent(Bool.self, forKey: .diagnosticsEnabled)
?? defaults.diagnosticsEnabled
hasSeenOnboarding = try container.decodeIfPresent(Bool.self, forKey: .hasSeenOnboarding)
?? defaults.hasSeenOnboarding
onboardingCompleted = try container.decodeIfPresent(Bool.self, forKey: .onboardingCompleted)
?? defaults.onboardingCompleted
hotkeyAwarenessConfirmed = try container.decodeIfPresent(Bool.self, forKey: .hotkeyAwarenessConfirmed)
?? defaults.hotkeyAwarenessConfirmed
provocationStylePreset = try container.decodeIfPresent(
ProvocationStylePreset.self, forKey: .provocationStylePreset
) ?? defaults.provocationStylePreset
customStyleInstructions = try container.decodeIfPresent(String.self, forKey: .customStyleInstructions)
?? defaults.customStyleInstructions
automaticallyCheckForUpdates = try container.decodeIfPresent(
Bool.self, forKey: .automaticallyCheckForUpdates
) ?? defaults.automaticallyCheckForUpdates
appUpdateChannel = try container.decodeIfPresent(AppUpdateChannel.self, forKey: .appUpdateChannel)
?? defaults.appUpdateChannel
aiTimeoutSeconds = try container.decodeIfPresent(TimeInterval.self, forKey: .aiTimeoutSeconds)
?? defaults.aiTimeoutSeconds
}
}

Expand Down Expand Up @@ -493,8 +517,12 @@ public extension AppSettings {
result.hotkeyModifiers = Self.defaultHotkeyModifiers
}

result.prompt1 = String(result.prompt1.trimmingCharacters(in: .whitespacesAndNewlines).prefix(Self.maxPromptLength))
result.prompt2 = String(result.prompt2.trimmingCharacters(in: .whitespacesAndNewlines).prefix(Self.maxPromptLength))
result.prompt1 = String(
result.prompt1.trimmingCharacters(in: .whitespacesAndNewlines).prefix(Self.maxPromptLength)
)
result.prompt2 = String(
result.prompt2.trimmingCharacters(in: .whitespacesAndNewlines).prefix(Self.maxPromptLength)
)

if result.prompt1.isEmpty {
result.prompt1 = Self.defaultPrompt1
Expand Down
3 changes: 2 additions & 1 deletion FreeThinker/Core/Services/DefaultAIService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,8 @@ public actor DefaultAIService: AIServiceProtocol {

let content = try parser.parse(rawOutput: rawOutput)
Logger.info(
"Generated provocation requestId=\(request.id.uuidString) durationMs=\(Int(clock.now().timeIntervalSince(startedAt) * 1_000))",
"Generated provocation requestId=\(request.id.uuidString) "
+ "durationMs=\(Int(clock.now().timeIntervalSince(startedAt) * 1_000))",
category: .aiService
)

Expand Down
6 changes: 5 additions & 1 deletion FreeThinker/Core/Services/FoundationModelsAdapter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,11 @@ public actor FoundationModelsAdapter: FoundationModelsAdapterProtocol {

do {
let response = try await session.respond(to: normalizedPrompt)
let trimmed = String(response.content.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines).prefix(options.maximumOutputCharacters))
let trimmed = String(
response.content
.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
.prefix(options.maximumOutputCharacters)
)
guard !trimmed.isEmpty else {
throw FreeThinkerError.generationFailed
}
Expand Down
5 changes: 4 additions & 1 deletion FreeThinker/Core/Services/ProvocationOrchestrator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -400,7 +400,10 @@ private extension ProvocationOrchestrator {
if error == .cancelled || Task.isCancelled {
metrics.cancellationCount += 1
let reason = pendingCancellationReason?.rawValue ?? "task-cancelled"
Logger.info("Pipeline cancelled source=\(source.rawValue) reason=\(reason)", category: .orchestrator)
Logger.info(
"Pipeline cancelled source=\(source.rawValue) reason=\(reason)",
category: .orchestrator
)
recordDiagnostic(
stage: .aiGeneration,
category: .warning,
Expand Down
4 changes: 3 additions & 1 deletion FreeThinker/Core/Services/ProvocationPromptComposer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ public struct ProvocationPromptComposer: ProvocationPromptComposing, Sendable {
let customInstruction = normalizedCustomInstruction(normalizedSettings.customStyleInstructions)

Logger.debug(
"Composing prompt type=\(request.provocationType.rawValue) style=\(normalizedSettings.provocationStylePreset.rawValue) textChars=\(selectedText.count)",
"Composing prompt type=\(request.provocationType.rawValue) "
+ "style=\(normalizedSettings.provocationStylePreset.rawValue) "
+ "textChars=\(selectedText.count)",
category: .promptComposer
)

Expand Down
13 changes: 9 additions & 4 deletions FreeThinker/Core/Utilities/ErrorPresentationMapper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,13 @@ public struct ErrorPresentationMapper: ErrorPresentationMapping {
switch error {
case .accessibilityPermissionDenied:
let translocationHint = isTranslocatedProvider()
? " FreeThinker appears to be running from a translocated location. Move it to /Applications, relaunch, then re-enable Accessibility once."
? " FreeThinker appears to be running from a translocated location."
+ " Move it to /Applications, relaunch, then re-enable Accessibility once."
: ""
return ErrorPresentation(
message: "FreeThinker needs Accessibility access. Open Settings -> Privacy & Security -> Accessibility, then enable FreeThinker.\(translocationHint)",
message: "FreeThinker needs Accessibility access. "
+ "Open Settings -> Privacy & Security -> Accessibility, then enable FreeThinker."
+ "\(translocationHint)",
action: .openAccessibilitySettings,
preferPanelPresentation: true
)
Expand Down Expand Up @@ -110,14 +113,16 @@ public struct ErrorPresentationMapper: ErrorPresentationMapping {

case .hotkeyRegistrationConflict:
return ErrorPresentation(
message: "That shortcut is already used by another app. Open Settings to change or disable the FreeThinker hotkey.",
message: "That shortcut is already used by another app. "
+ "Open Settings to change or disable the FreeThinker hotkey.",
action: .openHotkeySettings,
preferPanelPresentation: true
)

case .hotkeyRegistrationFailed:
return ErrorPresentation(
message: "FreeThinker could not register its global hotkey. Open Settings to retry or adjust the shortcut.",
message: "FreeThinker could not register its global hotkey. "
+ "Open Settings to retry or adjust the shortcut.",
action: .openHotkeySettings,
preferPanelPresentation: true
)
Expand Down
8 changes: 6 additions & 2 deletions FreeThinker/UI/FloatingPanel/FloatingPanelView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -131,8 +131,12 @@ public struct FloatingPanelView: View {
.buttonStyle(.plain)
.keyboardShortcut("p", modifiers: [.command])
.accessibilityIdentifier(FloatingPanelAccessibility.Identifier.pinButton)
.accessibilityLabel(viewModel.isPinned ? FloatingPanelAccessibility.Label.unpin : FloatingPanelAccessibility.Label.pin)
.accessibilityHint(viewModel.isPinned ? FloatingPanelAccessibility.Hint.unpin : FloatingPanelAccessibility.Hint.pin)
.accessibilityLabel(
viewModel.isPinned ? FloatingPanelAccessibility.Label.unpin : FloatingPanelAccessibility.Label.pin
)
.accessibilityHint(
viewModel.isPinned ? FloatingPanelAccessibility.Hint.unpin : FloatingPanelAccessibility.Hint.pin
)
}

private var closeButton: some View {
Expand Down
6 changes: 4 additions & 2 deletions FreeThinker/UI/Settings/AccessibilityHelpSettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ public struct AccessibilityHelpSettingsView: View {
}
.buttonStyle(.borderedProminent)

Text("If permission does not persist across relaunch, move FreeThinker to /Applications, relaunch, then enable Accessibility again.")
Text("If permission does not persist across relaunch, move FreeThinker to /Applications, "
+ "relaunch, then enable Accessibility again.")
.font(.footnote)
.foregroundStyle(.secondary)
}
Expand All @@ -39,7 +40,8 @@ public struct AccessibilityHelpSettingsView: View {

private extension AccessibilityHelpSettingsView {
func openAccessibilitySettings() {
guard let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility") else {
let urlString = "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility"
guard let url = URL(string: urlString) else {
return
}
NSWorkspace.shared.open(url)
Expand Down
7 changes: 5 additions & 2 deletions FreeThinker/UI/Settings/ProvocationSettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,9 @@ public struct ProvocationSettingsView: View {
.frame(minHeight: 160)
.font(.body)
.focused($isCustomInstructionEditorFocused)
.accessibilityIdentifier(SettingsAccessibility.Identifier.provocationCustomInstructionEditor)
.accessibilityIdentifier(
SettingsAccessibility.Identifier.provocationCustomInstructionEditor
)
.onChange(of: draftCustomInstructions) { oldValue, newValue in
scheduleCustomInstructionPersistence(newValue)
}
Expand Down Expand Up @@ -69,7 +71,8 @@ public struct ProvocationSettingsView: View {
}

if isOverCustomInstructionLimit {
Text("Limit is \(AppSettings.maxCustomInstructionLength) characters. Extra text is trimmed when saved.")
Text("Limit is \(AppSettings.maxCustomInstructionLength) characters. "
+ "Extra text is trimmed when saved.")
.font(.footnote)
.foregroundStyle(.orange)
}
Expand Down
Loading