Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -56,16 +56,16 @@ extension ComposerView {
if let trigger = emojiTriggerRange() {
let results = EmojiCatalog.search(query: trigger.filter)
if !results.isEmpty {
withAnimation(VAnimation.fast) { showEmojiMenu = true }
showEmojiMenu = true
if emojiFilter != trigger.filter {
emojiSelectedIndex = 0
}
emojiFilter = trigger.filter
} else {
withAnimation(VAnimation.fast) { showEmojiMenu = false }
showEmojiMenu = false
}
} else {
withAnimation(VAnimation.fast) { showEmojiMenu = false }
showEmojiMenu = false
}
}

Expand All @@ -83,7 +83,7 @@ extension ComposerView {

textReplacer.replaceText?(nsRange, entry.emoji)

withAnimation(VAnimation.fast) { showEmojiMenu = false }
showEmojiMenu = false
emojiSelectedIndex = 0
}

Expand All @@ -101,7 +101,7 @@ extension ComposerView {
case .tab:
selectEmoji(filtered[emojiSelectedIndex])
case .dismiss:
withAnimation(VAnimation.fast) { showEmojiMenu = false }
showEmojiMenu = false
suppressEmojiReopen = true
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,21 +92,21 @@ extension ComposerView {
let filter = String(text.dropFirst())
let filtered = filteredSlashCommands(filter)
if !filtered.isEmpty {
withAnimation(VAnimation.fast) { showSlashMenu = true }
showSlashMenu = true
if slashFilter != filter {
slashSelectedIndex = 0
}
slashFilter = filter
} else {
withAnimation(VAnimation.fast) { showSlashMenu = false }
showSlashMenu = false
}
} else {
withAnimation(VAnimation.fast) { showSlashMenu = false }
showSlashMenu = false
}
}

func selectSlashCommand(_ command: SlashCommand) {
withAnimation(VAnimation.fast) { showSlashMenu = false }
showSlashMenu = false
slashSelectedIndex = 0
inputText = Self.slashCommandInputTextForSelection(command)
if command.shouldAutoSendOnSelect {
Expand All @@ -132,9 +132,9 @@ extension ComposerView {
suppressSlashReopen = true
}
inputText = newText
withAnimation(VAnimation.fast) { showSlashMenu = false }
showSlashMenu = false
case .dismiss:
withAnimation(VAnimation.fast) { showSlashMenu = false }
showSlashMenu = false
inputText = ""
}
}
Expand Down
43 changes: 34 additions & 9 deletions clients/macos/vellum-assistant/Features/Chat/ComposerView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,24 @@ import AppKit

private let composerLog = Logger(subsystem: Bundle.appBundleIdentifier, category: "Composer")

@MainActor
private final class ComposerMenuRefreshScheduler {
private var generation = 0

func schedule(_ action: @escaping @MainActor () -> Void) {
generation += 1
let currentGeneration = generation
DispatchQueue.main.async { [weak self] in
guard let self, self.generation == currentGeneration else { return }
action()
}
}

func cancel() {
generation += 1
}
}

struct ComposerView: View {
private let composerMaxHeight: CGFloat = 300
private let composerActionButtonSize: CGFloat = 32
Expand Down Expand Up @@ -84,6 +102,7 @@ struct ComposerView: View {
@State var emojiFilter = ""
@State var emojiSelectedIndex = 0
@State var textReplacer = TextReplacementProxy()
@State private var menuRefreshScheduler = ComposerMenuRefreshScheduler()
/// Snapshot of inputText captured when dictation starts, used to restore on cancel.
@State private var preDictationText: String = ""
/// Live amplitude from VoiceInputManager, bypassing ChatViewModel's 100ms coalescing.
Expand All @@ -102,6 +121,18 @@ struct ComposerView: View {
return nil
}

private func scheduleComposerMenuRefresh() {
menuRefreshScheduler.schedule {
if inputText.isEmpty {
showSlashMenu = false
showEmojiMenu = false
} else {
updateSlashState()
updateEmojiState()
}
}
}

var body: some View {
VStack(spacing: VSpacing.sm) {
// Slash command popup (above the composer)
Expand Down Expand Up @@ -141,8 +172,6 @@ struct ComposerView: View {
}
#endif
.fixedSize(horizontal: false, vertical: true)
.animation(VAnimation.fast, value: showSlashMenu)
.animation(VAnimation.fast, value: showEmojiMenu)
.padding(.horizontal, VSpacing.lg)
.padding(.top, VSpacing.sm)
.frame(maxWidth: VSpacing.chatColumnMaxWidth)
Expand Down Expand Up @@ -177,6 +206,7 @@ struct ComposerView: View {
if enabled, !hasPendingConfirmation {
composerFocus = true
} else if !enabled {
menuRefreshScheduler.cancel()
composerFocus = false
showSlashMenu = false
showEmojiMenu = false
Expand Down Expand Up @@ -331,16 +361,11 @@ struct ComposerView: View {
composerFocus = true
}
.onChange(of: inputText) {
if inputText.isEmpty {
withAnimation(VAnimation.fast) { showSlashMenu = false; showEmojiMenu = false }
} else {
updateSlashState()
updateEmojiState()
}
scheduleComposerMenuRefresh()
}
.onChange(of: cursorPosition) {
if !inputText.isEmpty {
updateEmojiState()
scheduleComposerMenuRefresh()
}
}
}
Expand Down