From 9ba29444d2a63bf78437c650a0810c72b18d6b85 Mon Sep 17 00:00:00 2001 From: lk340 Date: Thu, 17 Jul 2025 15:22:03 -0400 Subject: [PATCH 1/3] Update AccessibliityNotificationsManager and KeyboardShortcutsManager with pinned text highlight updates. * `AccessibliityNotificationsManager` now properly handles removing `InputView` from the UI when it matches the current highlighted text, and the current highlighted text is about to be removed. * `AccessibliityNotificationsManager` also now handles setting the unpinned highlighted context and showing it in the UI. * `KeyboardShortcutsManager` now clears all states related to highlighted text. --- .../AccessibilityNotificationsManager.swift | 23 ++++++++++++++++--- .../KeyboardShortcutsManager.swift | 4 ++-- .../Onit/UI/Panels/State/OnitPanelState.swift | 4 ++++ 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/macos/Onit/Accessibility/Notifications/AccessibilityNotificationsManager.swift b/macos/Onit/Accessibility/Notifications/AccessibilityNotificationsManager.swift index 94dca9da..240586bc 100644 --- a/macos/Onit/Accessibility/Notifications/AccessibilityNotificationsManager.swift +++ b/macos/Onit/Accessibility/Notifications/AccessibilityNotificationsManager.swift @@ -595,6 +595,24 @@ class AccessibilityNotificationsManager: ObservableObject { showDebug() } + + private func clearPendingInputsOnInvalidSelectedText() { + let currentHighlightedTextIsInView = PanelStateCoordinator.shared.state.selectedPendingInput == PanelStateCoordinator.shared.state.unpinnedPendingInput + + + /// Clearing `InputView` when it's showing the current highlighted text that's about to be cleared. + if currentHighlightedTextIsInView { + PanelStateCoordinator.shared.state.selectedPendingInput = nil + } + + PanelStateCoordinator.shared.state.unpinnedPendingInput = nil + PanelStateCoordinator.shared.state.trackedPendingInput = nil + } + + private func addSelectedTextToContextAndShowInView(_ input: Input) { + PanelStateCoordinator.shared.state.selectedPendingInput = input + PanelStateCoordinator.shared.state.unpinnedPendingInput = input + } private func processSelectedText(_ text: String?) { guard Defaults[.autoContextFromHighlights], @@ -602,8 +620,7 @@ class AccessibilityNotificationsManager: ObservableObject { HighlightedTextValidator.isValid(text: selectedText) else { screenResult.userInteraction.selectedText = nil - PanelStateCoordinator.shared.state.pendingInput = nil - PanelStateCoordinator.shared.state.trackedPendingInput = nil + clearPendingInputsOnInvalidSelectedText() return } @@ -612,7 +629,7 @@ class AccessibilityNotificationsManager: ObservableObject { let input = Input(selectedText: selectedText, application: currentSource ?? "") if Defaults[.autoAddHighlightedTextToContext] { - PanelStateCoordinator.shared.state.pendingInput = input + addSelectedTextToContextAndShowInView(input) } else { PanelStateCoordinator.shared.state.trackedPendingInput = input } diff --git a/macos/Onit/KeyboardShortcuts/KeyboardShortcutsManager.swift b/macos/Onit/KeyboardShortcuts/KeyboardShortcutsManager.swift index 1d6b6e9e..37b706cf 100644 --- a/macos/Onit/KeyboardShortcuts/KeyboardShortcutsManager.swift +++ b/macos/Onit/KeyboardShortcuts/KeyboardShortcutsManager.swift @@ -106,8 +106,8 @@ struct KeyboardShortcutsManager { if state.panel != nil { if state.showContextMenuBrowserTabs { state.showContextMenuBrowserTabs = false - } else if state.pendingInput != nil { - state.pendingInput = nil + } else if state.hasHighlightedText { + state.clearHighlightedTextStates() } else { PanelStateCoordinator.shared.closePanel() } diff --git a/macos/Onit/UI/Panels/State/OnitPanelState.swift b/macos/Onit/UI/Panels/State/OnitPanelState.swift index cba07a14..037587f6 100644 --- a/macos/Onit/UI/Panels/State/OnitPanelState.swift +++ b/macos/Onit/UI/Panels/State/OnitPanelState.swift @@ -136,6 +136,10 @@ class OnitPanelState: NSObject { } var pinnedPendingInputs: [Input] = [] + + var hasHighlightedText: Bool { + selectedPendingInput != nil || trackedPendingInput != nil || unpinnedPendingInput != nil || !pinnedPendingInputs.isEmpty + } func clearHighlightedTextStates() { selectedPendingInput = nil From 8de5deceebf9a8eb424d8342eca5e9ee163ebad5 Mon Sep 17 00:00:00 2001 From: lk340 Date: Thu, 17 Jul 2025 15:38:23 -0400 Subject: [PATCH 2/3] Add UI Updates * Add pin icon. * ContextTag can now handle the pin action and shows pinned corner icon when pinned. * ExternalTetheredButton now accounts for both auto-highlighted and tracked highlighted text. * InputView `smallRemove` button now clears the unpinned highlighted text context tag when it matches the removed InputView text. * Update FinalContextView to now show full array of input (highlighted text) contexts. * Update PromptCore to properly show the selected highlighted text context. --- .../Icons/pin.imageset/Contents.json | 15 +++++ .../Icons/pin.imageset/pin.svg | 3 + macos/Onit/Data/Structures/Input.swift | 2 +- macos/Onit/UI/Components/ContextTag.swift | 61 +++++++++++++++---- .../UI/Content/ExternalTetheredButton.swift | 2 +- macos/Onit/UI/Prompt/FinalContextView.swift | 8 ++- .../UI/Prompt/Generated/GeneratedView.swift | 2 +- macos/Onit/UI/Prompt/Input/InputButtons.swift | 14 ++++- macos/Onit/UI/Prompt/PromptCore.swift | 4 +- 9 files changed, 89 insertions(+), 22 deletions(-) create mode 100644 macos/Onit/Assets.xcassets/Icons/pin.imageset/Contents.json create mode 100644 macos/Onit/Assets.xcassets/Icons/pin.imageset/pin.svg diff --git a/macos/Onit/Assets.xcassets/Icons/pin.imageset/Contents.json b/macos/Onit/Assets.xcassets/Icons/pin.imageset/Contents.json new file mode 100644 index 00000000..3dad0e97 --- /dev/null +++ b/macos/Onit/Assets.xcassets/Icons/pin.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "pin.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/macos/Onit/Assets.xcassets/Icons/pin.imageset/pin.svg b/macos/Onit/Assets.xcassets/Icons/pin.imageset/pin.svg new file mode 100644 index 00000000..e14da339 --- /dev/null +++ b/macos/Onit/Assets.xcassets/Icons/pin.imageset/pin.svg @@ -0,0 +1,3 @@ + + + diff --git a/macos/Onit/Data/Structures/Input.swift b/macos/Onit/Data/Structures/Input.swift index 7024dcee..13219bff 100644 --- a/macos/Onit/Data/Structures/Input.swift +++ b/macos/Onit/Data/Structures/Input.swift @@ -7,7 +7,7 @@ import Foundation -struct Input: Codable, Equatable { +struct Input: Codable, Equatable, Hashable { var selectedText: String var application: String? } diff --git a/macos/Onit/UI/Components/ContextTag.swift b/macos/Onit/UI/Components/ContextTag.swift index 2a19b3f4..6248c535 100644 --- a/macos/Onit/UI/Components/ContextTag.swift +++ b/macos/Onit/UI/Components/ContextTag.swift @@ -19,10 +19,12 @@ struct ContextTag: View { private let shouldFadeIn: Bool private let iconBundleURL: URL? private let iconView: (any View)? + private let iconViewCornerIcon: ImageResource? private let caption: String? private let tooltip: String? private let errorDotColor: Color? private let action: (() -> Void)? + private let pinAction: (() -> Void)? private let removeAction: (() -> Void)? init( @@ -37,10 +39,12 @@ struct ContextTag: View { shouldFadeIn: Bool = false, iconBundleURL: URL? = nil, iconView: (any View)? = nil, + iconViewCornerIcon: ImageResource? = nil, caption: String? = nil, tooltip: String? = nil, errorDotColor: Color? = nil, action: (() -> Void)? = nil, + pinAction: (() -> Void)? = nil, removeAction: (() -> Void)? = nil ) { self.text = text @@ -54,10 +58,12 @@ struct ContextTag: View { self.shouldFadeIn = shouldFadeIn self.iconBundleURL = iconBundleURL self.iconView = iconView + self.iconViewCornerIcon = iconViewCornerIcon self.caption = caption self.tooltip = tooltip self.errorDotColor = errorDotColor self.action = action + self.pinAction = pinAction self.removeAction = removeAction } @@ -72,6 +78,10 @@ struct ContextTag: View { return NSWorkspace.shared.icon(forFile: bundleUrl.path) } + private var hasHoverActions: Bool { + pinAction != nil || removeAction != nil + } + var body: some View { ZStack(alignment: .leading) { HStack(alignment: .center, spacing: 6) { @@ -97,7 +107,21 @@ struct ContextTag: View { } if let iconView = iconView { - AnyView(iconView) + ZStack(alignment: .bottomTrailing) { + AnyView(iconView) + + if let cornerIcon = iconViewCornerIcon { + ZStack(alignment: .center) { + Circle() + .fill(isHoveredBody ? hoverBackground : background) + .frame(width: 13, height: 13) + + Image(cornerIcon) + .addIconStyles(iconSize: 7.45) + } + .offset(x: 4, y: 4) + } + } } if isLoading { textView.shimmering() } @@ -114,16 +138,29 @@ struct ContextTag: View { } } - HStack(spacing: 0) { - Spacer() - - if let removeAction = removeAction { + if hasHoverActions { + HStack(spacing: 0) { + Spacer() + FadeHorizontal(color: hoverBackground) - removeButton(removeAction) + + HStack(spacing: 6) { + if let pinAction = pinAction { + hoverActionButton(icon: .pin) { + pinAction() + } + } + + if let removeAction = removeAction { + hoverActionButton(icon: .cross) { + removeAction() + } + } + } } + .frame(height: height) + .opacity(isHoveredBody ? 1 : 0) } - .frame(height: height) - .opacity(isHoveredBody ? 1 : 0) } .padding(.leading, 4) .padding(.trailing, 6) @@ -179,13 +216,13 @@ extension ContextTag { .addAnimation(dependency: [isHoveredBody, isHoveredRemove]) } - private func removeButton(_ removeAction: @escaping () -> Void) -> some View { + private func hoverActionButton(icon: ImageResource, hoverAction: @escaping () -> Void) -> some View { Button { - removeAction() + hoverAction() } label: { - Image(.cross) + Image(icon) .addIconStyles( - foregroundColor: isHoveredRemove ? .white : .gray100, + foregroundColor: isHoveredRemove ? Color.primary : .gray100, iconSize: 9 ) .addAnimation(dependency: isHoveredRemove) diff --git a/macos/Onit/UI/Content/ExternalTetheredButton.swift b/macos/Onit/UI/Content/ExternalTetheredButton.swift index 82163f38..9ac2c414 100644 --- a/macos/Onit/UI/Content/ExternalTetheredButton.swift +++ b/macos/Onit/UI/Content/ExternalTetheredButton.swift @@ -54,7 +54,7 @@ struct ExternalTetheredButton: View { } private var capturedHighlightedText: Bool { - return windowState?.pendingInput != nil + return windowState?.unpinnedPendingInput != nil || windowState?.trackedPendingInput != nil } private var foregroundWindowIcon: NSImage? { diff --git a/macos/Onit/UI/Prompt/FinalContextView.swift b/macos/Onit/UI/Prompt/FinalContextView.swift index 0fc57b05..be38c41a 100644 --- a/macos/Onit/UI/Prompt/FinalContextView.swift +++ b/macos/Onit/UI/Prompt/FinalContextView.swift @@ -25,7 +25,7 @@ struct FinalContextView: View { let prompt: Prompt var usingContextOrInput: Bool { - usingContext || prompt.input != nil + usingContext || !prompt.inputs.isEmpty } var usingContext: Bool { @@ -164,8 +164,10 @@ struct FinalContextView: View { .buttonStyle(.plain) if isExpanded { - if let input = prompt.input { - InputView(input: input, isEditing: false) + if !prompt.inputs.isEmpty { + ForEach(prompt.inputs, id: \.self) { input in + InputView(input: input, isEditing: false) + } } if usingContext { diff --git a/macos/Onit/UI/Prompt/Generated/GeneratedView.swift b/macos/Onit/UI/Prompt/Generated/GeneratedView.swift index ecac38a4..61e05b20 100644 --- a/macos/Onit/UI/Prompt/Generated/GeneratedView.swift +++ b/macos/Onit/UI/Prompt/Generated/GeneratedView.swift @@ -36,7 +36,7 @@ struct GeneratedView: View { #if DEBUG #Preview { var prompt = Prompt.sample - prompt.input = Input(selectedText: "blablabla", application: "Xcode") + prompt.inputs = [Input(selectedText: "blablabla", application: "Xcode")] return GeneratedView(prompt: prompt) } diff --git a/macos/Onit/UI/Prompt/Input/InputButtons.swift b/macos/Onit/UI/Prompt/Input/InputButtons.swift index e8672a58..31d56678 100644 --- a/macos/Onit/UI/Prompt/Input/InputButtons.swift +++ b/macos/Onit/UI/Prompt/Input/InputButtons.swift @@ -17,9 +17,11 @@ struct InputButtons: View { var body: some View { Group { - if let state = state, input == state.pendingInput { + if let state = state, input == state.selectedPendingInput { Button { - state.pendingInput = nil + clearHighlightedText(state: state) + + state.selectedPendingInput = nil } label: { Image(.smallRemove) .renderingMode(.template) @@ -57,6 +59,14 @@ struct InputButtons: View { Defaults[.showHighlightedTextInput] = false } } + + // MARK: - Private Functions + + private func clearHighlightedText(state: OnitPanelState) { + if state.selectedPendingInput == state.unpinnedPendingInput { + state.unpinnedPendingInput = nil + } + } } #if DEBUG diff --git a/macos/Onit/UI/Prompt/PromptCore.swift b/macos/Onit/UI/Prompt/PromptCore.swift index 2db00dc4..88d8cc7f 100644 --- a/macos/Onit/UI/Prompt/PromptCore.swift +++ b/macos/Onit/UI/Prompt/PromptCore.swift @@ -70,8 +70,8 @@ struct PromptCore: View { var body: some View { VStack(spacing: 0) { - if let windowState = windowState, let pendingInput = windowState.pendingInput { - InputView(input: pendingInput) + if let windowState = windowState, let selectedPendingInput = windowState.selectedPendingInput { + InputView(input: selectedPendingInput) } VStack(spacing: 6) { From 9753d0d1d30c810a99aa1b2a669813b1c4b035d7 Mon Sep 17 00:00:00 2001 From: lk340 Date: Thu, 17 Jul 2025 16:21:17 -0400 Subject: [PATCH 3/3] Make isHovered state more clear in ContextTag. --- macos/Onit/UI/Components/ContextTag.swift | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/macos/Onit/UI/Components/ContextTag.swift b/macos/Onit/UI/Components/ContextTag.swift index 6248c535..c8b6dc94 100644 --- a/macos/Onit/UI/Components/ContextTag.swift +++ b/macos/Onit/UI/Components/ContextTag.swift @@ -69,7 +69,7 @@ struct ContextTag: View { @State private var isHoveredBody: Bool = false @State private var isPressedBody: Bool = false - @State private var isHoveredRemove: Bool = false + @State private var isHoveredAction: Bool = false private let height: CGFloat = 24 @@ -210,10 +210,10 @@ extension ContextTag { Text(text) .styleText( size: 12, - color: isHoveredRemove ? .T_3 : isHoveredBody ? hoverTextColor : textColor + color: isHoveredAction ? .T_3 : isHoveredBody ? hoverTextColor : textColor ) .truncateText() - .addAnimation(dependency: [isHoveredBody, isHoveredRemove]) + .addAnimation(dependency: [isHoveredBody, isHoveredAction]) } private func hoverActionButton(icon: ImageResource, hoverAction: @escaping () -> Void) -> some View { @@ -222,14 +222,14 @@ extension ContextTag { } label: { Image(icon) .addIconStyles( - foregroundColor: isHoveredRemove ? Color.primary : .gray100, + foregroundColor: isHoveredAction ? Color.primary : .gray100, iconSize: 9 ) - .addAnimation(dependency: isHoveredRemove) + .addAnimation(dependency: isHoveredAction) } .background(hoverBackground) .onHover { isHovering in - isHoveredRemove = isHovering + isHoveredAction = isHovering } } }