diff --git a/macos/Onit/Accessibility/Notifications/AccessibilityNotificationsManager.swift b/macos/Onit/Accessibility/Notifications/AccessibilityNotificationsManager.swift index d5d09f3b..94dca9da 100644 --- a/macos/Onit/Accessibility/Notifications/AccessibilityNotificationsManager.swift +++ b/macos/Onit/Accessibility/Notifications/AccessibilityNotificationsManager.swift @@ -603,13 +603,19 @@ class AccessibilityNotificationsManager: ObservableObject { screenResult.userInteraction.selectedText = nil PanelStateCoordinator.shared.state.pendingInput = nil + PanelStateCoordinator.shared.state.trackedPendingInput = nil return } screenResult.userInteraction.selectedText = selectedText let input = Input(selectedText: selectedText, application: currentSource ?? "") - PanelStateCoordinator.shared.state.pendingInput = input + + if Defaults[.autoAddHighlightedTextToContext] { + PanelStateCoordinator.shared.state.pendingInput = input + } else { + PanelStateCoordinator.shared.state.trackedPendingInput = input + } } // MARK: Caret Position Handling diff --git a/macos/Onit/Data/Persistence/Defaults.swift b/macos/Onit/Data/Persistence/Defaults.swift index ac45f55f..b5674740 100644 --- a/macos/Onit/Data/Persistence/Defaults.swift +++ b/macos/Onit/Data/Persistence/Defaults.swift @@ -114,6 +114,9 @@ extension Defaults.Keys { static let lineHeight = Key("lineHeight", default: 1.5) static let voiceSilenceThreshold = Key("voiceSilenceThreshold", default: -40) static let voiceSpeechPassThreshold = Key("voiceSpeechPassThreshold", default: 0.7) + /// Highlighted Text + static let showHighlightedTextInput = Key("showHighlightedTextInput", default: true) + static let autoAddHighlightedTextToContext = Key("autoAddHighlightedTextToContext", default: true) // Local model advanced options static let localKeepAlive = Key("localKeepAlive", default: nil) diff --git a/macos/Onit/Helpers/StringHelpers.swift b/macos/Onit/Helpers/StringHelpers.swift new file mode 100644 index 00000000..0c05601a --- /dev/null +++ b/macos/Onit/Helpers/StringHelpers.swift @@ -0,0 +1,14 @@ +// +// StringHelpers.swift +// Onit +// +// Created by Loyd Kim on 7/18/25. +// + +struct StringHelpers { + static func removeWhiteSpaceAndNewLines(_ str: String) -> String { + return str.replacingOccurrences(of: "\\r?\\n", with: " ", options: .regularExpression) + .replacingOccurrences(of: " +", with: " ", options: .regularExpression) + .trimmingCharacters(in: .whitespacesAndNewlines) + } +} diff --git a/macos/Onit/UI/Components/ContextTag.swift b/macos/Onit/UI/Components/ContextTag.swift index 2a19b3f4..7ec0e887 100644 --- a/macos/Onit/UI/Components/ContextTag.swift +++ b/macos/Onit/UI/Components/ContextTag.swift @@ -14,9 +14,11 @@ struct ContextTag: View { private let background: Color private let hoverBackground: Color private let hasHoverBorder: Bool + private let hasDottedBorder: Bool private let maxWidth: CGFloat private let isLoading: Bool private let shouldFadeIn: Bool + private let borderColor: Color? private let iconBundleURL: URL? private let iconView: (any View)? private let caption: String? @@ -32,9 +34,11 @@ struct ContextTag: View { background: Color = .gray500, hoverBackground: Color = .gray400, hasHoverBorder: Bool = false, + hasDottedBorder: Bool = false, maxWidth: CGFloat = 155, isLoading: Bool = false, shouldFadeIn: Bool = false, + borderColor: Color? = nil, iconBundleURL: URL? = nil, iconView: (any View)? = nil, caption: String? = nil, @@ -49,9 +53,11 @@ struct ContextTag: View { self.background = background self.hoverBackground = hoverBackground self.hasHoverBorder = hasHoverBorder + self.hasDottedBorder = hasDottedBorder self.maxWidth = maxWidth self.isLoading = isLoading self.shouldFadeIn = shouldFadeIn + self.borderColor = borderColor self.iconBundleURL = iconBundleURL self.iconView = iconView self.caption = caption @@ -151,8 +157,8 @@ struct ContextTag: View { .addAnimation(dependency: isHoveredBody) .addBorder( cornerRadius: 4, - stroke: hasHoverBorder && isHoveredBody ? .T_4 : .clear, - dotted: true + stroke: hasHoverBorder && isHoveredBody ? .T_4 : borderColor ?? .clear, + dotted: hasDottedBorder ) .addButtonEffects( background: background, diff --git a/macos/Onit/UI/Panels/State/OnitPanelState.swift b/macos/Onit/UI/Panels/State/OnitPanelState.swift index 49df4982..ece64a2f 100644 --- a/macos/Onit/UI/Panels/State/OnitPanelState.swift +++ b/macos/Onit/UI/Panels/State/OnitPanelState.swift @@ -130,6 +130,8 @@ class OnitPanelState: NSObject { } } + var trackedPendingInput: Input? = nil + var systemPromptId: String = SystemPrompt.outputOnly.id var imageUploads: [URL: UploadProgress] = [:] diff --git a/macos/Onit/UI/Prompt/Files/FileRow.swift b/macos/Onit/UI/Prompt/Files/FileRow.swift index a942864e..80f4625c 100644 --- a/macos/Onit/UI/Prompt/Files/FileRow.swift +++ b/macos/Onit/UI/Prompt/Files/FileRow.swift @@ -14,6 +14,9 @@ struct FileRow: View { @ObservedObject private var debugManager = DebugManager.shared @Default(.autoContextFromCurrentWindow) var autoContextFromCurrentWindow + @Default(.autoAddHighlightedTextToContext) var autoAddHighlightedTextToContext + @Default(.autoContextFromHighlights) var autoContextFromHighlights + @Default(.showHighlightedTextInput) var showHighlightedTextInput @State private var ocrComparisonResult: OCRComparisonResult? = nil @State private var showOCRDetails = false @@ -119,9 +122,11 @@ struct FileRow: View { FlowLayout(spacing: 6) { PaperclipButton() - addForegroundWindowToContextButton + addWindowToContextButton + addHighlightedTextToContextButton pendingWindowContextItems addedWindowContextItems + highlightedTextContext } ocrDetailsLink @@ -149,28 +154,71 @@ struct FileRow: View { // MARK: - Child Components extension FileRow { + private func ghostContextTag( + text: String, + iconBundleURL: URL? = nil, + iconView: (any View)? = nil, + tooltip: String, + action: @escaping () -> Void + ) -> some View { + ContextTag( + text: text, + textColor: .T_2, + hoverTextColor: .white, + background: contextTagBackground, + hoverBackground: contextTagHoverBackground, + hasHoverBorder: true, + hasDottedBorder: true, + shouldFadeIn: true, + iconBundleURL: iconBundleURL, + iconView: iconView, + tooltip: tooltip + ) { + action() + } + } + @ViewBuilder - private var addForegroundWindowToContextButton: some View { + private var addHighlightedTextToContextButton: some View { + if accessibilityEnabled, + autoContextFromHighlights, + let windowState = windowState, + let trackedPendingInput = windowState.trackedPendingInput + { + ghostContextTag( + text: StringHelpers.removeWhiteSpaceAndNewLines(trackedPendingInput.selectedText), + iconView: Image(.text).addIconStyles(iconSize: 14), + tooltip: "Add Highlighted Text To Context" + ) { + windowState.pendingInput = trackedPendingInput + windowState.trackedPendingInput = nil + } + .onChange(of: autoAddHighlightedTextToContext) { _, autoAddHighlightedText in + if autoAddHighlightedText { + windowState.pendingInput = trackedPendingInput + windowState.trackedPendingInput = nil + } + } + } + } + + @ViewBuilder + private var addWindowToContextButton: some View { if accessibilityEnabled, autoContextFromCurrentWindow, !(windowBeingAddedToContext || windowAlreadyInContext), - let foregroundWindow = windowState?.foregroundWindow + let windowState = windowState, + let foregroundWindow = windowState.foregroundWindow { let foregroundWindowName = WindowHelpers.getWindowName(window: foregroundWindow.element) let iconBundleURL = WindowHelpers.getWindowAppBundleUrl(window: foregroundWindow.element) - - ContextTag( + + ghostContextTag( text: contextTagText, - textColor: .T_2, - hoverTextColor: .white, - background: contextTagBackground, - hoverBackground: contextTagHoverBackground, - hasHoverBorder: true, - shouldFadeIn: true, iconBundleURL: iconBundleURL, tooltip: "Add \(foregroundWindowName) Context" ) { - windowState?.addWindowToContext(window: foregroundWindow.element) + windowState.addWindowToContext(window: foregroundWindow.element) } } } @@ -202,6 +250,21 @@ extension FileRow { } } + @ViewBuilder + private var highlightedTextContext: some View { + if let pendingInput = windowState?.pendingInput { + ContextTag( + text: StringHelpers.removeWhiteSpaceAndNewLines(pendingInput.selectedText), + borderColor: showHighlightedTextInput ? .gray400 : .clear, + iconView: Image(.text).addIconStyles(iconSize: 14) + ) { + showHighlightedTextInput = true + } removeAction: { + windowState?.pendingInput = nil + } + } + } + @ViewBuilder private var addedWindowContextItems: some View { if !contextList.isEmpty { diff --git a/macos/Onit/UI/Prompt/Input/InputBody.swift b/macos/Onit/UI/Prompt/Input/InputBody.swift index c68bcaf5..be39ef6c 100644 --- a/macos/Onit/UI/Prompt/Input/InputBody.swift +++ b/macos/Onit/UI/Prompt/Input/InputBody.swift @@ -22,7 +22,7 @@ struct InputBody: View { } var height: CGFloat { - min(textHeight, 73) + min(textHeight, 222) } var body: some View { diff --git a/macos/Onit/UI/Prompt/Input/InputButtons.swift b/macos/Onit/UI/Prompt/Input/InputButtons.swift index c3c38aca..9502384f 100644 --- a/macos/Onit/UI/Prompt/Input/InputButtons.swift +++ b/macos/Onit/UI/Prompt/Input/InputButtons.swift @@ -5,6 +5,7 @@ // Created by Benjamin Sage on 10/8/24. // +import Defaults import SwiftUI struct InputButtons: View { @@ -13,6 +14,7 @@ struct InputButtons: View { @Binding var inputExpanded: Bool var input: Input + var isEditing: Bool var body: some View { Group { @@ -26,18 +28,19 @@ struct InputButtons: View { .buttonStyle(DarkerButtonStyle()) } - CopyButton(text: input.selectedText) - .frame(width: 20, height: 20) - Button { - inputExpanded.toggle() + if isEditing { + Defaults[.showHighlightedTextInput] = false + } else { + inputExpanded.toggle() + } } label: { Color.clear .frame(width: 20, height: 20) .overlay { Image(.smallChevRight) .renderingMode(.template) - .rotationEffect(inputExpanded ? .degrees(90) : .zero) + .rotationEffect(isEditing ? .degrees(90) : inputExpanded ? .degrees(-90) : .degrees(90)) } } } @@ -47,6 +50,6 @@ struct InputButtons: View { #if DEBUG #Preview { - InputButtons(inputExpanded: .constant(true), input: .sample) + InputButtons(inputExpanded: .constant(true), input: .sample, isEditing: true) } #endif diff --git a/macos/Onit/UI/Prompt/Input/InputTitle.swift b/macos/Onit/UI/Prompt/Input/InputTitle.swift index b2dee297..a8ebc7b5 100644 --- a/macos/Onit/UI/Prompt/Input/InputTitle.swift +++ b/macos/Onit/UI/Prompt/Input/InputTitle.swift @@ -10,30 +10,31 @@ import SwiftUI struct InputTitle: View { @Binding var inputExpanded: Bool var input: Input + var isEditing: Bool var sourceString: String { guard let sourceText = input.application else { return "" } - return " - \(sourceText)" + return " [\(sourceText)]" } var inputString: String { - "Input\(sourceString)" + "From\(sourceString)" } var body: some View { - HStack(spacing: 8) { + HStack(alignment: .center, spacing: 8) { Text(inputString) - .appFont(.medium13) + .appFont(.medium12) .textSelection(.enabled) Spacer() - InputButtons(inputExpanded: $inputExpanded, input: input) + InputButtons(inputExpanded: $inputExpanded, input: input, isEditing: isEditing) } .foregroundStyle(.gray100) .padding(.horizontal, 12) - .padding(.vertical, 8) + .padding(.vertical, 4) } } #Preview { - InputTitle(inputExpanded: .constant(true), input: .sample) + InputTitle(inputExpanded: .constant(true), input: .sample, isEditing: true) } diff --git a/macos/Onit/UI/Prompt/Input/InputView.swift b/macos/Onit/UI/Prompt/Input/InputView.swift index 92234111..b3632da0 100644 --- a/macos/Onit/UI/Prompt/Input/InputView.swift +++ b/macos/Onit/UI/Prompt/Input/InputView.swift @@ -5,31 +5,32 @@ // Created by Benjamin Sage on 10/3/24. // +import Defaults import SwiftUI struct InputView: View { - - @State var inputExpanded: Bool = true - var input: Input var isEditing: Bool = true + + @State var inputExpanded: Bool = false var body: some View { VStack(spacing: 0) { - InputTitle(inputExpanded: $inputExpanded, input: input) + InputTitle(inputExpanded: $inputExpanded, input: input, isEditing: isEditing) divider InputBody(inputExpanded: $inputExpanded, input: input) } .background { - RoundedRectangle(cornerRadius: 10) - .fill(.gray800) - .strokeBorder(.gray600) + RoundedRectangle(cornerRadius: 6) + .fill(.gray500) + .strokeBorder(.gray400) } - .padding([.horizontal, .top], isEditing ? 12 : 0) + .padding(.top, isEditing ? 6 : 0) + .padding([.horizontal], isEditing ? 8 : 0) } var divider: some View { - Color.gray600 + Color.gray400 .frame(height: 1) .opacity(inputExpanded ? 1 : 0) } diff --git a/macos/Onit/UI/Prompt/PromptCore.swift b/macos/Onit/UI/Prompt/PromptCore.swift index 2db00dc4..1250ff36 100644 --- a/macos/Onit/UI/Prompt/PromptCore.swift +++ b/macos/Onit/UI/Prompt/PromptCore.swift @@ -17,6 +17,7 @@ struct PromptCore: View { @Default(.mode) var mode @Default(.showTwoWeekProTrialEndedAlert) var showTwoWeekProTrialEndedAlert + @Default(.showHighlightedTextInput) var showHighlightedTextInput private var chats: [Chat] { let chatsFilteredByAccount = allChats @@ -70,10 +71,6 @@ struct PromptCore: View { var body: some View { VStack(spacing: 0) { - if let windowState = windowState, let pendingInput = windowState.pendingInput { - InputView(input: pendingInput) - } - VStack(spacing: 6) { contextAndInput PromptCoreFooter( @@ -176,7 +173,11 @@ extension PromptCore { } private var contextAndInput: some View { - VStack(alignment: .leading, spacing: 8) { + VStack(alignment: .leading, spacing: 0) { + if showHighlightedTextInput, let windowState = windowState, let pendingInput = windowState.pendingInput { + InputView(input: pendingInput, inputExpanded: true) + } + if !appState.subscriptionPlanError.isEmpty { Text(appState.subscriptionPlanError) .styleText( @@ -186,10 +187,15 @@ extension PromptCore { ) } - FileRow(contextList: windowState?.pendingContextList ?? []) - textField + VStack(alignment: .leading, spacing: 8) { + FileRow(contextList: windowState?.pendingContextList ?? []) + textField + } + .padding(.top, 6) + .padding([.horizontal, .bottom], 12) } - .padding(12) + .padding(.top, 2) + .padding([.horizontal, .bottom], 0) .background(.gray800) .addGradientBorder( cornerRadius: 8, diff --git a/macos/Onit/UI/Settings/GeneralTab.swift b/macos/Onit/UI/Settings/GeneralTab.swift index 45814d37..5d0b902f 100644 --- a/macos/Onit/UI/Settings/GeneralTab.swift +++ b/macos/Onit/UI/Settings/GeneralTab.swift @@ -18,6 +18,8 @@ struct GeneralTab: View { @Default(.tetheredButtonHiddenApps) var tetheredButtonHiddenApps @Default(.tetheredButtonHideAllApps) var tetheredButtonHideAllApps @Default(.tetheredButtonHideAllAppsTimerDate) var tetheredButtonHideAllAppsTimerDate + @Default(.showHighlightedTextInput) var showHighlightedTextInput + @Default(.autoAddHighlightedTextToContext) var autoAddHighlightedTextToContext @State var isLaunchAtStartupEnabled: Bool = SMAppService.mainApp.status == .enabled @State var isAnalyticsEnabled: Bool = PostHogSDK.shared.isOptOut() == false @@ -73,6 +75,8 @@ struct GeneralTab: View { appearanceSection GeneralTabVoice() + + showHighlightedTextInputSection hiddenAppsSection @@ -414,6 +418,39 @@ struct GeneralTab: View { } } + var showHighlightedTextInputSection: some View { + SettingsSection( + iconImage: .text, + title: "Highlighted Text" + ) { + VStack(alignment: .leading, spacing: 20) { + HStack { + Text("Show highlighted text input") + .font(.system(size: 13)) + + Spacer() + + Toggle("", isOn: $showHighlightedTextInput) + .toggleStyle(.switch) + .controlSize(.small) + } + } + + VStack(alignment: .leading, spacing: 20) { + HStack { + Text("Auto-add highlighted text to context.") + .font(.system(size: 13)) + + Spacer() + + Toggle("", isOn: $autoAddHighlightedTextToContext) + .toggleStyle(.switch) + .controlSize(.small) + } + } + } + } + var hiddenAppsSection: some View { SettingsSection( iconSystem: "eye.slash",