From a12e4aa3b882b74f0a6f95d2f4c6c760465f33b2 Mon Sep 17 00:00:00 2001 From: Auto Date: Thu, 29 Jan 2026 08:28:48 +0200 Subject: [PATCH] refactor(ui): extract keyboard utilities and add padding constant - Create shared `isSubmitEnter()` utility in `ui/src/lib/keyboard.ts` for IME-aware Enter key handling across all input components - Extract magic number 48 to named constant `COLLAPSED_DEBUG_PANEL_CLEARANCE` with explanatory comment (40px panel header + 8px margin) - Update 5 components to use the new utility: - AssistantChat.tsx - ExpandProjectChat.tsx - SpecCreationChat.tsx - FolderBrowser.tsx - TerminalTabs.tsx This follows up on PR #121 which added IME composition checks. The refactoring centralizes the logic for easier maintenance and documents the padding value that prevents Kanban cards from being cut off when the debug panel is collapsed. Co-Authored-By: Claude Opus 4.5 --- ui/src/App.tsx | 5 +++- ui/src/components/AssistantChat.tsx | 4 +-- ui/src/components/ExpandProjectChat.tsx | 4 +-- ui/src/components/FolderBrowser.tsx | 4 +-- ui/src/components/SpecCreationChat.tsx | 4 +-- ui/src/components/TerminalTabs.tsx | 4 +-- ui/src/lib/keyboard.ts | 38 +++++++++++++++++++++++++ 7 files changed, 52 insertions(+), 11 deletions(-) create mode 100644 ui/src/lib/keyboard.ts diff --git a/ui/src/App.tsx b/ui/src/App.tsx index 4a830f8..ddde90f 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -36,6 +36,9 @@ import { Badge } from '@/components/ui/badge' const STORAGE_KEY = 'autocoder-selected-project' const VIEW_MODE_KEY = 'autocoder-view-mode' +// Bottom padding for main content when debug panel is collapsed (40px header + 8px margin) +const COLLAPSED_DEBUG_PANEL_CLEARANCE = 48 + function App() { // Initialize selected project from localStorage const [selectedProject, setSelectedProject] = useState(() => { @@ -331,7 +334,7 @@ function App() { {/* Main Content */}
{!selectedProject ? (
diff --git a/ui/src/components/AssistantChat.tsx b/ui/src/components/AssistantChat.tsx index a2d7ba8..a9d8b5f 100644 --- a/ui/src/components/AssistantChat.tsx +++ b/ui/src/components/AssistantChat.tsx @@ -12,6 +12,7 @@ import { useAssistantChat } from '../hooks/useAssistantChat' import { ChatMessage as ChatMessageComponent } from './ChatMessage' import { ConversationHistory } from './ConversationHistory' import type { ChatMessage } from '../lib/types' +import { isSubmitEnter } from '../lib/keyboard' import { Button } from '@/components/ui/button' import { Textarea } from '@/components/ui/textarea' @@ -134,8 +135,7 @@ export function AssistantChat({ } const handleKeyDown = (e: React.KeyboardEvent) => { - // Skip if composing (e.g., Japanese IME input) - if (e.key === 'Enter' && !e.shiftKey && !e.nativeEvent.isComposing) { + if (isSubmitEnter(e)) { e.preventDefault() handleSend() } diff --git a/ui/src/components/ExpandProjectChat.tsx b/ui/src/components/ExpandProjectChat.tsx index 2eb30b3..d1ccd21 100644 --- a/ui/src/components/ExpandProjectChat.tsx +++ b/ui/src/components/ExpandProjectChat.tsx @@ -11,6 +11,7 @@ import { useExpandChat } from '../hooks/useExpandChat' import { ChatMessage } from './ChatMessage' import { TypingIndicator } from './TypingIndicator' import type { ImageAttachment } from '../lib/types' +import { isSubmitEnter } from '../lib/keyboard' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Card, CardContent } from '@/components/ui/card' @@ -88,8 +89,7 @@ export function ExpandProjectChat({ } const handleKeyDown = (e: React.KeyboardEvent) => { - // Skip if composing (e.g., Japanese IME input) - if (e.key === 'Enter' && !e.shiftKey && !e.nativeEvent.isComposing) { + if (isSubmitEnter(e)) { e.preventDefault() handleSendMessage() } diff --git a/ui/src/components/FolderBrowser.tsx b/ui/src/components/FolderBrowser.tsx index 8641bdd..2e8171a 100644 --- a/ui/src/components/FolderBrowser.tsx +++ b/ui/src/components/FolderBrowser.tsx @@ -18,6 +18,7 @@ import { ArrowLeft, } from 'lucide-react' import * as api from '../lib/api' +import { isSubmitEnter } from '../lib/keyboard' import type { DirectoryEntry, DriveInfo } from '../lib/types' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' @@ -269,8 +270,7 @@ export function FolderBrowser({ onSelect, onCancel, initialPath }: FolderBrowser className="flex-1" autoFocus onKeyDown={(e) => { - // Skip if composing (e.g., Japanese IME input) - if (e.key === 'Enter' && !e.nativeEvent.isComposing) handleCreateFolder() + if (isSubmitEnter(e, false)) handleCreateFolder() if (e.key === 'Escape') { setIsCreatingFolder(false) setNewFolderName('') diff --git a/ui/src/components/SpecCreationChat.tsx b/ui/src/components/SpecCreationChat.tsx index 24d0c09..c96a1f2 100644 --- a/ui/src/components/SpecCreationChat.tsx +++ b/ui/src/components/SpecCreationChat.tsx @@ -12,6 +12,7 @@ import { ChatMessage } from './ChatMessage' import { QuestionOptions } from './QuestionOptions' import { TypingIndicator } from './TypingIndicator' import type { ImageAttachment } from '../lib/types' +import { isSubmitEnter } from '../lib/keyboard' import { Button } from '@/components/ui/button' import { Textarea } from '@/components/ui/textarea' import { Card, CardContent } from '@/components/ui/card' @@ -127,8 +128,7 @@ export function SpecCreationChat({ } const handleKeyDown = (e: React.KeyboardEvent) => { - // Skip if composing (e.g., Japanese IME input) - if (e.key === 'Enter' && !e.shiftKey && !e.nativeEvent.isComposing) { + if (isSubmitEnter(e)) { e.preventDefault() handleSendMessage() } diff --git a/ui/src/components/TerminalTabs.tsx b/ui/src/components/TerminalTabs.tsx index 760d69d..c53ff22 100644 --- a/ui/src/components/TerminalTabs.tsx +++ b/ui/src/components/TerminalTabs.tsx @@ -8,6 +8,7 @@ import { useState, useRef, useEffect, useCallback } from 'react' import { Plus, X } from 'lucide-react' import type { TerminalInfo } from '@/lib/types' +import { isSubmitEnter } from '@/lib/keyboard' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' @@ -96,8 +97,7 @@ export function TerminalTabs({ // Handle key events during editing const handleKeyDown = useCallback( (e: React.KeyboardEvent) => { - // Skip if composing (e.g., Japanese IME input) - if (e.key === 'Enter' && !e.nativeEvent.isComposing) { + if (isSubmitEnter(e, false)) { e.preventDefault() submitEdit() } else if (e.key === 'Escape') { diff --git a/ui/src/lib/keyboard.ts b/ui/src/lib/keyboard.ts new file mode 100644 index 0000000..4e3cf71 --- /dev/null +++ b/ui/src/lib/keyboard.ts @@ -0,0 +1,38 @@ +/** + * Keyboard event utilities + * + * Helpers for handling keyboard events, particularly for IME-aware input handling. + */ + +/** + * Check if an Enter keypress should trigger form submission. + * + * Returns false during IME composition (e.g., Japanese, Chinese, Korean input) + * to prevent accidental submission while selecting characters. + * + * @param e - The keyboard event from React + * @param allowShiftEnter - If true, Shift+Enter returns false (for multiline input) + * @returns true if Enter should submit, false if it should be ignored + * + * @example + * // In a chat input (Shift+Enter for newline) + * if (isSubmitEnter(e)) { + * e.preventDefault() + * handleSend() + * } + * + * @example + * // In a single-line input (Enter always submits) + * if (isSubmitEnter(e, false)) { + * handleSubmit() + * } + */ +export function isSubmitEnter( + e: React.KeyboardEvent, + allowShiftEnter: boolean = true +): boolean { + if (e.key !== 'Enter') return false + if (allowShiftEnter && e.shiftKey) return false + if (e.nativeEvent.isComposing) return false + return true +}