Compare commits

...

3 Commits

Author SHA1 Message Date
abeatrix 4334bc84aa Correctly processes modifier combinations 2025-09-12 18:48:12 -07:00
abeatrix 7128eddff7 clean up 2025-09-12 18:46:45 -07:00
abeatrix 185082719f Add keyboard shortcuts and conditional keybinding display to chat action buttons
- Add KeybindingConfig interface for defining keyboard shortcuts
- Implement keyboard event handling for primary and secondary actions
- Show keybinding hints in button text only when navbar is hidden (VS Code detection)
- Add Escape key support for cancel actions in loading states
- Refactor button configuration to support keybinding metadata
2025-09-12 18:39:39 -07:00
2 changed files with 79 additions and 8 deletions
@@ -3,6 +3,7 @@ import type { Mode } from "@shared/storage/types"
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
import type React from "react"
import { useCallback, useEffect, useMemo, useRef } from "react"
import { usePlatform } from "@/context/PlatformContext"
import { ButtonActionType, getButtonConfig } from "../../shared/buttonConfig"
import type { ChatState, MessageHandlers } from "../../types/chatTypes"
@@ -32,6 +33,8 @@ export const ActionButtons: React.FC<ActionButtonsProps> = ({
}) => {
const { inputValue, selectedImages, selectedFiles, setSendingDisabled } = chatState
const isProcessingRef = useRef(false)
// HACK: Append keybinding only if the platform doesn't show navbar to determine if host is VS Code or not.
const showKeybindings = usePlatform().showNavbar !== true
// Memoize last messages to avoid unnecessary recalculations
const [lastMessage, secondLastMessage] = useMemo(() => {
@@ -41,7 +44,22 @@ export const ActionButtons: React.FC<ActionButtonsProps> = ({
// Memoize button configuration to avoid recalculation on every render
const buttonConfig = useMemo(() => {
return lastMessage ? getButtonConfig(lastMessage, mode) : { sendingDisabled: false, enableButtons: false }
if (!lastMessage) {
return { sendingDisabled: false, enableButtons: false }
}
// Append keybinding display to button text if available
const btnConfig = getButtonConfig(lastMessage, mode)
const config = { ...btnConfig } // Create a shallow copy to avoid mutating original
const primaryButtonKey = config.primaryKeybinding?.display
const secondaryButtonKey = config.secondaryKeybinding?.display
if (primaryButtonKey) {
config.primaryText = showKeybindings ? `${config.primaryText} (${primaryButtonKey})` : config.primaryText
}
if (secondaryButtonKey) {
config.secondaryText = showKeybindings ? `${config.secondaryText} (${secondaryButtonKey})` : config.secondaryText
}
// Return the modified config
return config
}, [lastMessage, mode])
// Single effect to handle all configuration updates
@@ -75,13 +93,56 @@ export const ActionButtons: React.FC<ActionButtonsProps> = ({
// Keyboard event handler
const handleKeyDown = useCallback(
(event: KeyboardEvent) => {
if (event.key === "Escape") {
event.preventDefault()
event.stopPropagation()
messageHandlers.executeButtonAction("cancel")
const checkKeybinding = (keybinding: string[]): boolean => {
if (keybinding.length === 1) {
return event.key === keybinding[0]
}
// Handle modifier combinations
const modifiers = keybinding.slice(0, -1)
const mainKey = keybinding[keybinding.length - 1]
// Check if all required modifiers are pressed
const modifierChecks = modifiers.every((modifier) => {
switch (modifier.toLowerCase()) {
case "control":
case "ctrl":
return event.ctrlKey
case "shift":
return event.shiftKey
case "alt":
case "option":
return event.altKey
case "meta":
case "cmd":
case "command":
return event.metaKey
default:
return false
}
})
return modifierChecks && event.key === mainKey
}
if (buttonConfig.primaryAction && buttonConfig.primaryKeybinding) {
if (checkKeybinding(buttonConfig.primaryKeybinding.key)) {
event.preventDefault()
event.stopPropagation()
messageHandlers.executeButtonAction(buttonConfig.primaryAction)
return
}
}
if (buttonConfig.secondaryAction && buttonConfig.secondaryKeybinding) {
if (checkKeybinding(buttonConfig.secondaryKeybinding.key)) {
event.preventDefault()
event.stopPropagation()
messageHandlers.executeButtonAction(buttonConfig.secondaryAction)
}
}
},
[messageHandlers],
[messageHandlers, buttonConfig],
)
useEffect(() => {
@@ -13,6 +13,12 @@ export type ButtonActionType =
| "utility" // Execute utility function (condense, report_bug)
| "retry" // Retry the last action
// e.g., { key: ["Escape"], display: "ESC" }
export interface KeybindingConfig {
key: string[]
display: string
}
/**
* Button configuration for different message states
*/
@@ -23,6 +29,8 @@ export interface ButtonConfig {
secondaryText?: string
primaryAction?: ButtonActionType
secondaryAction?: ButtonActionType
primaryKeybinding?: KeybindingConfig
secondaryKeybinding?: KeybindingConfig
}
/**
@@ -183,7 +191,8 @@ export const BUTTON_CONFIGS: Record<string, ButtonConfig> = {
sendingDisabled: true,
enableButtons: true,
primaryText: undefined,
secondaryText: "Cancel (ESC)",
secondaryText: "Cancel",
secondaryKeybinding: { key: ["Escape"], display: "ESC" },
primaryAction: undefined,
secondaryAction: "cancel",
},
@@ -201,7 +210,8 @@ export const BUTTON_CONFIGS: Record<string, ButtonConfig> = {
sendingDisabled: true,
enableButtons: true,
primaryText: undefined,
secondaryText: "Cancel (ESC)",
secondaryText: "Cancel",
secondaryKeybinding: { key: ["Escape"], display: "ESC" },
primaryAction: undefined,
secondaryAction: "cancel",
},