Merge 34e64cbe4dc7f134fe37e4ea12940cefcaaf3821 into 1f59e814f90c3f489f48d68262cb1bf640bf6181

This commit is contained in:
Vidit Kulshrestha 2026-08-02 00:59:24 +05:30 committed by GitHub
commit e4561b1ad1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -6,6 +6,9 @@
//
import SwiftUI
#if os(macOS)
import AppKit
#endif
struct CommandSuggestionsView: View {
@EnvironmentObject private var privateConversationModel: PrivateConversationModel
@ -14,6 +17,16 @@ struct CommandSuggestionsView: View {
@Binding var messageText: String
/// Row highlighted for keyboard navigation (macOS). Reset whenever the
/// filtered list changes so the highlight never outlives its list.
@State private var selectedIndex = 0
#if os(macOS)
/// Arrow keys never reach SwiftUI while the composer's field editor has
/// focus (the single-line field consumes moveUp:/moveDown: itself), so
/// navigation uses a local key monitor scoped to the panel's lifetime.
@State private var keyMonitor: Any?
#endif
/// The command already typed in full, once arguments have begun.
private var typedCommandAlias: String? {
guard messageText.hasPrefix("/"),
@ -43,23 +56,91 @@ struct CommandSuggestionsView: View {
// off-center.
if !filteredCommands.isEmpty {
let isUsageReminder = typedCommandAlias != nil
let commands = filteredCommands
VStack(alignment: .leading, spacing: 0) {
ForEach(filteredCommands) { command in
ForEach(Array(commands.enumerated()), id: \.element.id) { index, command in
Button {
// In usage-reminder mode the row is informational; an
// insert here would wipe the arguments being typed.
guard !isUsageReminder else { return }
messageText = command.alias + " "
accept(command)
} label: {
buttonRow(for: command)
}
.buttonStyle(.plain)
.background(rowHighlight(index: index, isUsageReminder: isUsageReminder))
}
}
.themedOverlayPanel()
.onChange(of: commands) { _ in
selectedIndex = 0
}
#if os(macOS)
.onAppear { installKeyMonitor() }
.onDisappear { removeKeyMonitor() }
#endif
}
}
/// Inserts the command with a trailing space, ready for arguments.
private func accept(_ command: CommandInfo) {
messageText = command.alias + " "
}
private func rowHighlight(index: Int, isUsageReminder: Bool) -> Color {
#if os(macOS)
if !isUsageReminder && index == selectedIndex {
return palette.secondary.opacity(0.15)
}
#endif
return .clear
}
#if os(macOS)
private func installKeyMonitor() {
guard keyMonitor == nil else { return }
keyMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { event in
handleKeyDown(event)
}
}
private func removeKeyMonitor() {
if let keyMonitor {
NSEvent.removeMonitor(keyMonitor)
}
keyMonitor = nil
}
/// Standard autocomplete navigation: arrows move the highlight,
/// return/tab insert the highlighted command. Returning nil consumes the
/// event, so return completes instead of sending while the list is up;
/// the usage-reminder row stays informational and passes everything
/// through (return sends the composed command).
private func handleKeyDown(_ event: NSEvent) -> NSEvent? {
let commands = filteredCommands
guard !commands.isEmpty,
typedCommandAlias == nil,
event.modifierFlags.intersection([.command, .option, .control]).isEmpty else {
return event
}
switch event.keyCode {
case 126: // up arrow
selectedIndex = max(0, selectedIndex - 1)
return nil
case 125: // down arrow
selectedIndex = min(commands.count - 1, selectedIndex + 1)
return nil
case 36, 48: // return, tab
accept(commands[min(selectedIndex, commands.count - 1)])
return nil
default:
return event
}
}
#endif
private func buttonRow(for command: CommandInfo) -> some View {
HStack {
Text(command.alias)