Fix screen-local submit shortcut regression (#2616)
This commit is contained in:
parent
8ae292e2b7
commit
8d90dd4e4b
|
|
@ -17,16 +17,14 @@ import { Sheet, SheetContent, SheetDescription, SheetTitle } from '@/components/
|
|||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import { VisuallyHidden } from 'radix-ui'
|
||||
import CommentMarkdown from '@/components/sidebar/CommentMarkdown'
|
||||
import { getShortcutPlatform } from '@/lib/shortcut-platform'
|
||||
import { isScreenSubmitShortcut } from '@/lib/screen-submit-shortcut'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useAppStore } from '@/store'
|
||||
import type {
|
||||
GitLabPipelineJob,
|
||||
GitLabWorkItem,
|
||||
GitLabWorkItemDetails,
|
||||
MRComment
|
||||
} from '../../../shared/types'
|
||||
import { keybindingMatchesAction } from '../../../shared/keybindings'
|
||||
|
||||
type Props = {
|
||||
item: GitLabWorkItem | null
|
||||
|
|
@ -153,7 +151,6 @@ export default function GitLabItemDialog({
|
|||
onClose,
|
||||
onCreateWorkspace
|
||||
}: Props): React.JSX.Element {
|
||||
const keybindings = useAppStore((s) => s.keybindings)
|
||||
const [details, setDetails] = useState<GitLabWorkItemDetails | null>(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
|
@ -425,16 +422,9 @@ export default function GitLabItemDialog({
|
|||
disabled={commentSubmitting}
|
||||
className="min-h-9 w-full resize-none rounded-md border border-input bg-transparent px-2.5 py-1.5 text-sm shadow-xs focus:border-ring focus:outline-none focus:ring-[3px] focus:ring-ring/50"
|
||||
onKeyDown={(e) => {
|
||||
if (
|
||||
keybindingMatchesAction(
|
||||
'composer.submit',
|
||||
e,
|
||||
getShortcutPlatform(),
|
||||
keybindings
|
||||
) &&
|
||||
commentDraft.trim() &&
|
||||
!commentSubmitting
|
||||
) {
|
||||
// Why: this is local textarea submit behavior; Settings
|
||||
// keybindings only cover app commands.
|
||||
if (isScreenSubmitShortcut(e) && commentDraft.trim() && !commentSubmitting) {
|
||||
e.preventDefault()
|
||||
void handleSubmitComment()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,8 +24,7 @@ import { VisuallyHidden } from 'radix-ui'
|
|||
import CommentMarkdown from '@/components/sidebar/CommentMarkdown'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useAppStore } from '@/store'
|
||||
import { getShortcutPlatform } from '@/lib/shortcut-platform'
|
||||
import { useShortcutLabel } from '@/hooks/useShortcutLabel'
|
||||
import { getScreenSubmitShortcutLabel, isScreenSubmitShortcut } from '@/lib/screen-submit-shortcut'
|
||||
import { createBrowserUuid } from '@/lib/browser-uuid'
|
||||
import {
|
||||
useTeamStates,
|
||||
|
|
@ -44,7 +43,6 @@ import {
|
|||
linearIssueComments,
|
||||
linearUpdateIssue
|
||||
} from '@/runtime/runtime-linear-client'
|
||||
import { keybindingMatchesAction } from '../../../shared/keybindings'
|
||||
|
||||
function LinearIcon({ className }: { className?: string }): React.JSX.Element {
|
||||
return (
|
||||
|
|
@ -950,8 +948,7 @@ export function LinearIssueCommentFooter({
|
|||
variant?: 'compact' | 'linear-page'
|
||||
}): React.JSX.Element {
|
||||
const settings = useAppStore((s) => s.settings)
|
||||
const keybindings = useAppStore((s) => s.keybindings)
|
||||
const submitShortcutLabel = useShortcutLabel('composer.submit')
|
||||
const submitShortcutLabel = getScreenSubmitShortcutLabel()
|
||||
const [body, setBody] = useState('')
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
||||
|
|
@ -993,12 +990,12 @@ export function LinearIssueCommentFooter({
|
|||
|
||||
const handleKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent) => {
|
||||
if (keybindingMatchesAction('composer.submit', e, getShortcutPlatform(), keybindings)) {
|
||||
if (isScreenSubmitShortcut(e)) {
|
||||
e.preventDefault()
|
||||
handleSubmit()
|
||||
}
|
||||
},
|
||||
[handleSubmit, keybindings]
|
||||
[handleSubmit]
|
||||
)
|
||||
|
||||
if (variant === 'linear-page') {
|
||||
|
|
|
|||
|
|
@ -2,17 +2,24 @@
|
|||
composer card markup together so the inline and modal variants share one UI
|
||||
surface without splitting the controlled form into hard-to-follow fragments. */
|
||||
import React from 'react'
|
||||
import { Check, ChevronDown, FolderPlus, LoaderCircle, PlugZap, Settings2 } from 'lucide-react'
|
||||
import {
|
||||
Check,
|
||||
ChevronDown,
|
||||
CornerDownLeft,
|
||||
FolderPlus,
|
||||
LoaderCircle,
|
||||
PlugZap,
|
||||
Settings2
|
||||
} from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
import { ShortcutKeyCombo } from '@/components/ShortcutKeyCombo'
|
||||
import { useShortcutKeys } from '@/hooks/useShortcutLabel'
|
||||
import RepoCombobox from '@/components/repo/RepoCombobox'
|
||||
import AgentCombobox from '@/components/agent/AgentCombobox'
|
||||
import { AGENT_CATALOG } from '@/lib/agent-catalog'
|
||||
import { useAppStore } from '@/store'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { WORKSPACE_FILE_PATH_MIME } from '@/lib/workspace-file-drag'
|
||||
import { getScreenSubmitModifierLabel } from '@/lib/screen-submit-shortcut'
|
||||
import type {
|
||||
GitHubWorkItem,
|
||||
GitLabWorkItem,
|
||||
|
|
@ -239,7 +246,7 @@ export default function NewWorkspaceComposerCard({
|
|||
const openModal = useAppStore((s) => s.openModal)
|
||||
const defaultTuiAgent = useAppStore((s) => s.settings?.defaultTuiAgent ?? null)
|
||||
const updateSettings = useAppStore((s) => s.updateSettings)
|
||||
const submitShortcutKeys = useShortcutKeys('composer.submit')
|
||||
const submitShortcutModifierLabel = getScreenSubmitModifierLabel()
|
||||
const selectedRepoName = React.useMemo(() => {
|
||||
const repo = eligibleRepos.find((candidate) => candidate.id === repoId)
|
||||
return repo?.displayName ?? repo?.path ?? 'This repository'
|
||||
|
|
@ -639,9 +646,10 @@ export default function NewWorkspaceComposerCard({
|
|||
>
|
||||
{creating ? <LoaderCircle className="size-4 animate-spin" /> : null}
|
||||
Create Workspace
|
||||
{submitShortcutKeys.length > 0 ? (
|
||||
<ShortcutKeyCombo keys={submitShortcutKeys} className="ml-1 text-current/80" />
|
||||
) : null}
|
||||
<span className="ml-1 inline-flex items-center gap-0.5 rounded border border-white/20 px-1.5 py-0.5 text-[10px] font-medium leading-none text-current/80">
|
||||
<span>{submitShortcutModifierLabel}</span>
|
||||
<CornerDownLeft className="size-3" />
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -6,12 +6,8 @@ import AgentSettingsDialog from '@/components/agent/AgentSettingsDialog'
|
|||
import { useComposerState } from '@/hooks/useComposerState'
|
||||
import { AGENT_CATALOG } from '@/lib/agent-catalog'
|
||||
import type { LinkedWorkItemSummary } from '@/lib/new-workspace'
|
||||
import {
|
||||
shouldAllowComposerEnterSubmitTarget,
|
||||
shouldSuppressEnterSubmit
|
||||
} from '@/lib/new-workspace-enter-guard'
|
||||
import { getShortcutPlatform } from '@/lib/shortcut-platform'
|
||||
import { keybindingMatchesAction } from '../../../shared/keybindings'
|
||||
import { shouldAllowComposerEnterSubmitTarget } from '@/lib/new-workspace-enter-guard'
|
||||
import { isScreenSubmitShortcut } from '@/lib/screen-submit-shortcut'
|
||||
import type {
|
||||
TuiAgent,
|
||||
WorkspaceCreateTelemetrySource,
|
||||
|
|
@ -137,7 +133,6 @@ function QuickTabBody({
|
|||
const [quickAgentOverride, setQuickAgentOverride] = useState<TuiAgent | null | undefined>(
|
||||
undefined
|
||||
)
|
||||
const keybindings = useAppStore((s) => s.keybindings)
|
||||
const preferredQuickAgent = useMemo<TuiAgent | null>(() => {
|
||||
const pref = settings?.defaultTuiAgent
|
||||
if (pref === 'blank') {
|
||||
|
|
@ -161,8 +156,7 @@ function QuickTabBody({
|
|||
await submitQuick(quickAgent)
|
||||
}, [quickAgent, submitQuick])
|
||||
|
||||
// The configured submit shortcut creates the workspace; Esc first blurs the
|
||||
// focused input, matching the full-page composer.
|
||||
// Cmd/Ctrl+Enter submits, Esc first blurs the focused input (like the full page).
|
||||
useEffect(() => {
|
||||
if (!active) {
|
||||
return
|
||||
|
|
@ -192,7 +186,9 @@ function QuickTabBody({
|
|||
return
|
||||
}
|
||||
|
||||
if (!keybindingMatchesAction('composer.submit', event, getShortcutPlatform(), keybindings)) {
|
||||
// Why: workspace creation is screen-local submit behavior, not a
|
||||
// user-configurable app command.
|
||||
if (!isScreenSubmitShortcut(event)) {
|
||||
return
|
||||
}
|
||||
if (!shouldAllowComposerEnterSubmitTarget(target, composerRef.current)) {
|
||||
|
|
@ -201,15 +197,12 @@ function QuickTabBody({
|
|||
if (createDisabled) {
|
||||
return
|
||||
}
|
||||
if (shouldSuppressEnterSubmit(event, false)) {
|
||||
return
|
||||
}
|
||||
event.preventDefault()
|
||||
void handleCreate()
|
||||
}
|
||||
window.addEventListener('keydown', onKeyDown, { capture: true })
|
||||
return () => window.removeEventListener('keydown', onKeyDown, { capture: true })
|
||||
}, [active, composerRef, createDisabled, handleCreate, keybindings, onClose])
|
||||
}, [active, composerRef, createDisabled, handleCreate, onClose])
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -132,8 +132,7 @@ import type {
|
|||
TaskViewPresetId
|
||||
} from '../../../shared/types'
|
||||
import { shouldSuppressEnterSubmit } from '@/lib/new-workspace-enter-guard'
|
||||
import { getShortcutPlatform } from '@/lib/shortcut-platform'
|
||||
import { useShortcutLabel } from '@/hooks/useShortcutLabel'
|
||||
import { getScreenSubmitShortcutLabel, isScreenSubmitShortcut } from '@/lib/screen-submit-shortcut'
|
||||
import { useTeamStates } from '@/hooks/useIssueMetadata'
|
||||
import {
|
||||
linearCreateIssue,
|
||||
|
|
@ -146,7 +145,6 @@ import {
|
|||
restoreAvailableDefaultTaskProvider,
|
||||
resolveVisibleTaskProvider
|
||||
} from '../../../shared/task-providers'
|
||||
import { keybindingMatchesAction } from '../../../shared/keybindings'
|
||||
|
||||
type TaskSource = TaskProvider
|
||||
|
||||
|
|
@ -1947,7 +1945,6 @@ export default function TaskPage(): React.JSX.Element {
|
|||
const openTaskPage = useAppStore((s) => s.openTaskPage)
|
||||
const closeTaskPage = useAppStore((s) => s.closeTaskPage)
|
||||
const activeModal = useAppStore((s) => s.activeModal)
|
||||
const keybindings = useAppStore((s) => s.keybindings)
|
||||
const repos = useAppStore((s) => s.repos)
|
||||
const repoMap = useRepoMap()
|
||||
const openModal = useAppStore((s) => s.openModal)
|
||||
|
|
@ -1975,7 +1972,7 @@ export default function TaskPage(): React.JSX.Element {
|
|||
const patchLinearIssue = useAppStore((s) => s.patchLinearIssue)
|
||||
const checkLinearConnection = useAppStore((s) => s.checkLinearConnection)
|
||||
const refreshPreflightStatus = useAppStore((s) => s.refreshPreflightStatus)
|
||||
const submitShortcutLabel = useShortcutLabel('composer.submit')
|
||||
const submitShortcutLabel = getScreenSubmitShortcutLabel()
|
||||
const eligibleRepos = useMemo(() => repos.filter((repo) => isGitRepoKind(repo)), [repos])
|
||||
|
||||
// Why: initial selection resolution honors (1) an explicit preselection from
|
||||
|
|
@ -5918,9 +5915,7 @@ export default function TaskPage(): React.JSX.Element {
|
|||
<DialogContent
|
||||
className="sm:max-w-lg"
|
||||
onKeyDown={(event) => {
|
||||
if (
|
||||
keybindingMatchesAction('composer.submit', event, getShortcutPlatform(), keybindings)
|
||||
) {
|
||||
if (isScreenSubmitShortcut(event)) {
|
||||
event.preventDefault()
|
||||
void handleCreateNewIssue()
|
||||
}
|
||||
|
|
@ -6050,9 +6045,7 @@ export default function TaskPage(): React.JSX.Element {
|
|||
className="w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-xs transition-[color,box-shadow] outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 resize-none max-h-60 overflow-y-auto scrollbar-sleek"
|
||||
/>
|
||||
</div>
|
||||
{submitShortcutLabel !== 'Unassigned' ? (
|
||||
<p className="text-[10px] text-muted-foreground">{submitShortcutLabel} to submit.</p>
|
||||
) : null}
|
||||
<p className="text-[10px] text-muted-foreground">{submitShortcutLabel} to submit.</p>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
|
|
@ -6090,9 +6083,7 @@ export default function TaskPage(): React.JSX.Element {
|
|||
<DialogContent
|
||||
className="sm:max-w-lg"
|
||||
onKeyDown={(event) => {
|
||||
if (
|
||||
keybindingMatchesAction('composer.submit', event, getShortcutPlatform(), keybindings)
|
||||
) {
|
||||
if (isScreenSubmitShortcut(event)) {
|
||||
event.preventDefault()
|
||||
void handleCreateNewLinearIssue()
|
||||
}
|
||||
|
|
@ -6164,9 +6155,7 @@ export default function TaskPage(): React.JSX.Element {
|
|||
className="w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-xs transition-[color,box-shadow] outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 resize-none max-h-60 overflow-y-auto scrollbar-sleek"
|
||||
/>
|
||||
</div>
|
||||
{submitShortcutLabel !== 'Unassigned' ? (
|
||||
<p className="text-[10px] text-muted-foreground">{submitShortcutLabel} to submit.</p>
|
||||
) : null}
|
||||
<p className="text-[10px] text-muted-foreground">{submitShortcutLabel} to submit.</p>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import {
|
|||
ArrowRight,
|
||||
CircleCheck,
|
||||
Copy,
|
||||
CornerDownLeft,
|
||||
Crosshair,
|
||||
ExternalLink,
|
||||
Globe,
|
||||
|
|
@ -62,6 +63,7 @@ import {
|
|||
redactKagiSessionToken
|
||||
} from '../../../../shared/browser-url'
|
||||
import { keybindingMatchesAction } from '../../../../shared/keybindings'
|
||||
import { getScreenSubmitModifierLabel, isScreenSubmitShortcut } from '@/lib/screen-submit-shortcut'
|
||||
import {
|
||||
browserViewportPresetToOverride,
|
||||
getBrowserViewportPreset
|
||||
|
|
@ -320,10 +322,8 @@ function PendingBrowserAnnotationCard({
|
|||
}): React.JSX.Element {
|
||||
const [comment, setComment] = useState('')
|
||||
const [intent, setIntent] = useState<BrowserAnnotationIntent>('change')
|
||||
const keybindings = useAppStore((state) => state.keybindings)
|
||||
const submitShortcut = useShortcutLabel('composer.submit')
|
||||
const trimmed = comment.trim()
|
||||
const shortcutPlatform = getShortcutPlatform()
|
||||
const submitModifierLabel = getScreenSubmitModifierLabel()
|
||||
|
||||
return (
|
||||
<Popover
|
||||
|
|
@ -382,10 +382,7 @@ function PendingBrowserAnnotationCard({
|
|||
onCancel()
|
||||
return
|
||||
}
|
||||
if (
|
||||
keybindingMatchesAction('composer.submit', event, shortcutPlatform, keybindings) &&
|
||||
!event.nativeEvent.isComposing
|
||||
) {
|
||||
if (isScreenSubmitShortcut(event)) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
if (trimmed) {
|
||||
|
|
@ -438,7 +435,8 @@ function PendingBrowserAnnotationCard({
|
|||
<MessageSquarePlus className="size-3.5" />
|
||||
Add
|
||||
<span className="ml-1 inline-flex items-center gap-0.5 rounded border border-white/20 px-1.5 py-0.5 text-[10px] font-medium leading-none text-current/80">
|
||||
<span>{submitShortcut}</span>
|
||||
<span>{submitModifierLabel}</span>
|
||||
<CornerDownLeft className="size-3" />
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,12 +1,9 @@
|
|||
import { useEffect } from 'react'
|
||||
import { ChevronLeft, Loader2 } from 'lucide-react'
|
||||
import { ChevronLeft, CornerDownLeft, Loader2 } from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { isEditableTarget } from '@/lib/editable-target'
|
||||
import { getShortcutPlatform } from '@/lib/shortcut-platform'
|
||||
import { useShortcutKeys } from '@/hooks/useShortcutLabel'
|
||||
import { useAppStore } from '@/store'
|
||||
import { getScreenSubmitModifierLabel, isScreenSubmitShortcut } from '@/lib/screen-submit-shortcut'
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
import { ShortcutKeyCombo } from '@/components/ShortcutKeyCombo'
|
||||
import type { OnboardingState } from '../../../../shared/types'
|
||||
import { AgentStep } from './AgentStep'
|
||||
import { ThemeStep } from './ThemeStep'
|
||||
|
|
@ -15,7 +12,6 @@ import { IntegrationsStep } from './IntegrationsStep'
|
|||
import { RepoStep } from './RepoStep'
|
||||
import { STEPS, useOnboardingFlow } from './use-onboarding-flow'
|
||||
import logo from '../../../../../resources/logo.svg'
|
||||
import { keybindingMatchesAction } from '../../../../shared/keybindings'
|
||||
|
||||
const stepCopy = {
|
||||
agent: {
|
||||
|
|
@ -62,8 +58,7 @@ export default function OnboardingFlow({
|
|||
onSettingsDetourStart
|
||||
}: OnboardingFlowProps): React.JSX.Element {
|
||||
const flow = useOnboardingFlow(onboarding, onOnboardingChange, { onSettingsDetourStart })
|
||||
const keybindings = useAppStore((state) => state.keybindings)
|
||||
const continueShortcutKeys = useShortcutKeys('onboarding.continue')
|
||||
const continueShortcutModifierLabel = getScreenSubmitModifierLabel()
|
||||
const { currentStep, stepIndex, busyLabel } = flow
|
||||
const copy = stepCopy[currentStep.id]
|
||||
const shouldShowSetupAction =
|
||||
|
|
@ -82,9 +77,9 @@ export default function OnboardingFlow({
|
|||
if (isEditableTarget(event.target)) {
|
||||
return
|
||||
}
|
||||
if (
|
||||
!keybindingMatchesAction('onboarding.continue', event, getShortcutPlatform(), keybindings)
|
||||
) {
|
||||
// Why: onboarding continue is screen-local submit behavior, not a
|
||||
// user-configurable app command.
|
||||
if (!isScreenSubmitShortcut(event)) {
|
||||
return
|
||||
}
|
||||
event.preventDefault()
|
||||
|
|
@ -96,7 +91,7 @@ export default function OnboardingFlow({
|
|||
}
|
||||
window.addEventListener('keydown', onKeyDown, { capture: true })
|
||||
return () => window.removeEventListener('keydown', onKeyDown, { capture: true })
|
||||
}, [currentStep.id, flowNext, flowOpenFolder, keybindings])
|
||||
}, [currentStep.id, flowNext, flowOpenFolder])
|
||||
|
||||
return (
|
||||
<div className="scrollbar-sleek fixed inset-0 z-[100] overflow-auto bg-background text-foreground">
|
||||
|
|
@ -264,9 +259,10 @@ export default function OnboardingFlow({
|
|||
>
|
||||
{busyLabel ? <Loader2 className="size-4 animate-spin" /> : null}
|
||||
{primaryActionLabel}
|
||||
{continueShortcutKeys.length > 0 ? (
|
||||
<ShortcutKeyCombo keys={continueShortcutKeys} className="ml-1 text-current/80" />
|
||||
) : null}
|
||||
<span className="ml-1 inline-flex items-center gap-0.5 rounded border border-primary-foreground/20 px-1.5 py-0.5 text-[10px] font-medium leading-none text-current/80">
|
||||
<span>{continueShortcutModifierLabel}</span>
|
||||
<CornerDownLeft className="size-3" />
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -12,11 +12,9 @@ import { Button } from '@/components/ui/button'
|
|||
import { Input } from '@/components/ui/input'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
import { parseGitHubIssueOrPRLink, parseGitHubIssueOrPRNumber } from '@/lib/github-links'
|
||||
import { getShortcutPlatform } from '@/lib/shortcut-platform'
|
||||
import { useShortcutLabel } from '@/hooks/useShortcutLabel'
|
||||
import { getScreenSubmitShortcutLabel, isScreenSubmitShortcut } from '@/lib/screen-submit-shortcut'
|
||||
import { ExternalLink, LoaderCircle } from 'lucide-react'
|
||||
import type { WorktreeMeta } from '../../../../shared/types'
|
||||
import { keybindingMatchesAction } from '../../../../shared/keybindings'
|
||||
|
||||
function parseExplicitGitHubIssueUrl(input: string): string | null {
|
||||
const trimmed = input.trim()
|
||||
|
|
@ -34,8 +32,7 @@ const WorktreeMetaDialog = React.memo(function WorktreeMetaDialog() {
|
|||
const closeModal = useAppStore((s) => s.closeModal)
|
||||
const updateWorktreeMeta = useAppStore((s) => s.updateWorktreeMeta)
|
||||
const fetchIssue = useAppStore((s) => s.fetchIssue)
|
||||
const keybindings = useAppStore((s) => s.keybindings)
|
||||
const submitShortcutLabel = useShortcutLabel('composer.submit')
|
||||
const submitShortcutLabel = getScreenSubmitShortcutLabel()
|
||||
|
||||
const isEditMeta = activeModal === 'edit-meta'
|
||||
const isOpen = isEditMeta
|
||||
|
|
@ -179,17 +176,14 @@ const WorktreeMetaDialog = React.memo(function WorktreeMetaDialog() {
|
|||
|
||||
const handleCommentKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (
|
||||
e.key === 'Enter' &&
|
||||
(!e.shiftKey ||
|
||||
keybindingMatchesAction('composer.submit', e, getShortcutPlatform(), keybindings))
|
||||
) {
|
||||
const isPlainEnter = e.key === 'Enter' && !e.shiftKey && !e.altKey && !e.metaKey && !e.ctrlKey
|
||||
if (isPlainEnter || isScreenSubmitShortcut(e)) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
handleSave()
|
||||
}
|
||||
},
|
||||
[handleSave, keybindings]
|
||||
[handleSave]
|
||||
)
|
||||
|
||||
const handleIssueKeyDown = useCallback(
|
||||
|
|
@ -352,8 +346,7 @@ const WorktreeMetaDialog = React.memo(function WorktreeMetaDialog() {
|
|||
/>
|
||||
<p className="text-[10px] text-muted-foreground">
|
||||
Supports **markdown** — bold, lists, `code`, links. Press Enter or{' '}
|
||||
{submitShortcutLabel !== 'Unassigned' ? `${submitShortcutLabel} to save, ` : ''}
|
||||
Shift+Enter for a new line.
|
||||
{submitShortcutLabel} to save, Shift+Enter for a new line.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,55 @@
|
|||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import {
|
||||
getScreenSubmitModifierLabel,
|
||||
getScreenSubmitShortcutLabel,
|
||||
isScreenSubmitShortcut
|
||||
} from './screen-submit-shortcut'
|
||||
|
||||
function setUserAgent(userAgent: string): void {
|
||||
vi.stubGlobal('navigator', { userAgent })
|
||||
}
|
||||
|
||||
describe('screen submit shortcut', () => {
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
|
||||
it('uses Cmd+Enter on macOS', () => {
|
||||
setUserAgent('Macintosh')
|
||||
|
||||
expect(isScreenSubmitShortcut({ key: 'Enter', metaKey: true })).toBe(true)
|
||||
expect(isScreenSubmitShortcut({ key: 'Enter', ctrlKey: true })).toBe(false)
|
||||
expect(getScreenSubmitModifierLabel()).toBe('⌘')
|
||||
expect(getScreenSubmitShortcutLabel()).toBe('⌘ Enter')
|
||||
})
|
||||
|
||||
it('uses Ctrl+Enter off macOS', () => {
|
||||
setUserAgent('Linux')
|
||||
|
||||
expect(isScreenSubmitShortcut({ key: 'Enter', ctrlKey: true })).toBe(true)
|
||||
expect(isScreenSubmitShortcut({ key: 'Enter', metaKey: true })).toBe(false)
|
||||
expect(getScreenSubmitModifierLabel()).toBe('Ctrl')
|
||||
expect(getScreenSubmitShortcutLabel()).toBe('Ctrl+Enter')
|
||||
})
|
||||
|
||||
it('ignores shifted or alternate Enter chords', () => {
|
||||
setUserAgent('Linux')
|
||||
|
||||
expect(isScreenSubmitShortcut({ key: 'Enter', ctrlKey: true, shiftKey: true })).toBe(false)
|
||||
expect(isScreenSubmitShortcut({ key: 'Enter', ctrlKey: true, altKey: true })).toBe(false)
|
||||
expect(isScreenSubmitShortcut({ key: 'Escape', ctrlKey: true })).toBe(false)
|
||||
})
|
||||
|
||||
it('ignores composing Enter events', () => {
|
||||
setUserAgent('Linux')
|
||||
|
||||
expect(isScreenSubmitShortcut({ key: 'Enter', ctrlKey: true, isComposing: true })).toBe(false)
|
||||
expect(
|
||||
isScreenSubmitShortcut({
|
||||
key: 'Enter',
|
||||
ctrlKey: true,
|
||||
nativeEvent: { isComposing: true }
|
||||
})
|
||||
).toBe(false)
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
import { getShortcutPlatform } from './shortcut-platform'
|
||||
|
||||
type ScreenSubmitShortcutEvent = {
|
||||
key: string
|
||||
altKey?: boolean
|
||||
ctrlKey?: boolean
|
||||
metaKey?: boolean
|
||||
shiftKey?: boolean
|
||||
isComposing?: boolean
|
||||
nativeEvent?: {
|
||||
isComposing?: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export function isScreenSubmitShortcut(event: ScreenSubmitShortcutEvent): boolean {
|
||||
if (event.isComposing || event.nativeEvent?.isComposing) {
|
||||
return false
|
||||
}
|
||||
if (event.key !== 'Enter' || event.altKey || event.shiftKey) {
|
||||
return false
|
||||
}
|
||||
// Why: screen submit is form-local behavior, so it stays fixed to the
|
||||
// platform convention instead of reading user-configurable app keybindings.
|
||||
const platform = getShortcutPlatform()
|
||||
return platform === 'darwin'
|
||||
? Boolean(event.metaKey) && !event.ctrlKey
|
||||
: Boolean(event.ctrlKey) && !event.metaKey
|
||||
}
|
||||
|
||||
export function getScreenSubmitModifierLabel(): string {
|
||||
return getShortcutPlatform() === 'darwin' ? '⌘' : 'Ctrl'
|
||||
}
|
||||
|
||||
export function getScreenSubmitShortcutLabel(): string {
|
||||
return getShortcutPlatform() === 'darwin' ? '⌘ Enter' : 'Ctrl+Enter'
|
||||
}
|
||||
|
|
@ -10,7 +10,6 @@ export type KeybindingScope =
|
|||
| 'fileExplorer'
|
||||
| 'composer'
|
||||
| 'settings'
|
||||
| 'onboarding'
|
||||
|
||||
export type KeybindingContext = 'app' | 'terminal' | 'browser'
|
||||
|
||||
|
|
@ -73,9 +72,7 @@ export type KeybindingActionId =
|
|||
| 'fileExplorer.copyRelativePath'
|
||||
| 'fileExplorer.delete'
|
||||
| 'composer.addAttachment'
|
||||
| 'composer.submit'
|
||||
| 'settings.search'
|
||||
| 'onboarding.continue'
|
||||
| 'terminal.copySelection'
|
||||
| 'terminal.paste'
|
||||
| 'terminal.search'
|
||||
|
|
@ -576,14 +573,6 @@ export const KEYBINDING_DEFINITIONS: readonly KeybindingDefinition[] = [
|
|||
searchKeywords: ['shortcut', 'composer', 'attachment', 'upload'],
|
||||
defaultBindings: platformBindings(['Mod+U'])
|
||||
},
|
||||
{
|
||||
id: 'composer.submit',
|
||||
title: 'Submit Composer',
|
||||
group: 'Composer',
|
||||
scope: 'composer',
|
||||
searchKeywords: ['shortcut', 'composer', 'submit', 'send', 'comment'],
|
||||
defaultBindings: platformBindings(['Mod+Enter'])
|
||||
},
|
||||
{
|
||||
id: 'settings.search',
|
||||
title: 'Search Settings',
|
||||
|
|
@ -592,14 +581,6 @@ export const KEYBINDING_DEFINITIONS: readonly KeybindingDefinition[] = [
|
|||
searchKeywords: ['shortcut', 'settings', 'search', 'find'],
|
||||
defaultBindings: platformBindings(['Mod+F'])
|
||||
},
|
||||
{
|
||||
id: 'onboarding.continue',
|
||||
title: 'Continue Onboarding',
|
||||
group: 'Onboarding',
|
||||
scope: 'onboarding',
|
||||
searchKeywords: ['shortcut', 'onboarding', 'continue', 'next'],
|
||||
defaultBindings: platformBindings(['Mod+Enter'])
|
||||
},
|
||||
{
|
||||
id: 'terminal.copySelection',
|
||||
title: 'Copy terminal selection',
|
||||
|
|
|
|||
Loading…
Reference in New Issue