Skip to content
Open
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
6 changes: 6 additions & 0 deletions packages/workshop-frontend/src/ChatInterface.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ import { useActionEntries } from "./useActions";
import { useAlwaysApproveTag } from "./useAlwaysApproveTag";
import { useResolveAction } from "./useResolveAction";
import { safeExternalUrl } from "./utils/safeExternalUrl";
import { isComposingKeyEvent } from "./utils/imeComposition";
import { useAuthenticatedApi } from "./AuthContext";
import { useVendorBranding } from "./useVendorBranding";
import OutOfCreditsModal from "./components/billing/OutOfCreditsModal";
Expand Down Expand Up @@ -3146,6 +3147,9 @@ export const ChatInput = ({
}
}}
onKeyDown={(e) => {
// While an IME is composing (e.g. Japanese kana-kanji conversion), Enter and the
// arrow keys belong to the input method, not to us. Let them through untouched.
if (isComposingKeyEvent(e)) return;
if (slashCommandPicker.open && e.key === "Escape") {
e.preventDefault();
slashCommandPicker.dismiss();
Expand Down Expand Up @@ -6511,6 +6515,7 @@ function ChatInterface({
onChange={(e) => setRenamingInput(e.target.value)}
onClick={(e) => e.stopPropagation()}
onKeyDown={(e) => {
if (isComposingKeyEvent(e)) return;
if (e.key === "Enter") {
e.preventDefault();
handleSaveListRename(chat.id);
Expand Down Expand Up @@ -6730,6 +6735,7 @@ function ChatInterface({
value={titleInput}
onChange={(e) => setTitleInput(e.target.value)}
onKeyDown={(e) => {
if (isComposingKeyEvent(e)) return;
if (e.key === "Enter") handleSaveChatTitle();
if (e.key === "Escape") handleCancelTitleEdit();
}}
Expand Down
2 changes: 2 additions & 0 deletions packages/workshop-frontend/src/Connections.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
loadBindingCardData,
} from './components/BlueprintBindingCard'
import { reportIssue } from './errorReporting'
import { isComposingKeyEvent } from './utils/imeComposition'

interface ConnectionsProps {
overseer: RpcStub<Overseer>
Expand Down Expand Up @@ -266,6 +267,7 @@ export default function Connections({ overseer, gadget, chatId, authenticatedApi
value={editValue}
onChange={(e) => setEditValue(e.target.value)}
onKeyDown={(e) => {
if (isComposingKeyEvent(e)) return
if (e.key === 'Enter') handleEditSave(gk.name)
if (e.key === 'Escape') handleEditCancel()
}}
Expand Down
3 changes: 3 additions & 0 deletions packages/workshop-frontend/src/FileSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Dialog, DropdownMenu, useKumoToastManager } from '@cloudflare/kumo'
import { DotsThree, DownloadSimple, Pencil, Plus, Trash, X } from '@phosphor-icons/react'
import DeleteConfirmationDialog from './components/DeleteConfirmationDialog'
import { WorkshopButton, WorkshopIconButton, WorkshopInput } from './components/WorkshopControls'
import { isComposingKeyEvent } from './utils/imeComposition'

interface FileSidebarProps {
files: string[]
Expand Down Expand Up @@ -213,6 +214,7 @@ export default function FileSidebar({
value={newFileName}
onChange={(e) => setNewFileName(e.target.value)}
onKeyDown={(e) => {
if (isComposingKeyEvent(e)) return
if (e.key === 'Enter') {
e.preventDefault()
handleCreateFile()
Expand Down Expand Up @@ -347,6 +349,7 @@ function FileRow({
onChange={(event) => setRenameValue(event.target.value)}
onClick={(event) => event.stopPropagation()}
onKeyDown={(event) => {
if (isComposingKeyEvent(event)) return
if (event.key === 'Enter') {
event.preventDefault()
onRenameSubmit(renameValue)
Expand Down
2 changes: 2 additions & 0 deletions packages/workshop-frontend/src/GadgetEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ import WorkspaceOpenErrorPage from './components/WorkspaceOpenErrorPage'
import { useWorkspaceOpen } from './useWorkspaceOpen'
import { reportIssue } from './errorReporting'
import GadgetExportMenu from './GadgetExportMenu'
import { isComposingKeyEvent } from './utils/imeComposition'

const NO_GADGETS: ReadonlySet<WorkpieceId> = new Set()

Expand Down Expand Up @@ -1348,6 +1349,7 @@ export default function GadgetEditor() {
value={titleInput}
onChange={e => setTitleInput(e.target.value)}
onKeyDown={e => {
if (isComposingKeyEvent(e)) return
if (e.key === 'Enter') handleSaveTitle()
if (e.key === 'Escape') handleCancelEdit()
}}
Expand Down
2 changes: 2 additions & 0 deletions packages/workshop-frontend/src/SettingsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { useAvatar, invalidateAvatarCache } from './useAvatar'
import { compressAvatar, avatarBlobUrl } from './avatarUtils'
import UsageSettings from './components/billing/UsageSettings'
import { useDocumentTitle } from './useDocumentTitle'
import { isComposingKeyEvent } from './utils/imeComposition'

// Shared, on-language control classes (match the rest of the app: Workspaces/Blueprints headers,
// the gatekeepers toolbar, the command palette). Kept here so the profile page reads as part of the
Expand Down Expand Up @@ -311,6 +312,7 @@ export default function SettingsPage() {
value={nameInput}
onChange={(e) => setNameInput(e.target.value)}
onKeyDown={(e) => {
if (isComposingKeyEvent(e)) return
if (e.key === 'Enter') handleSaveName()
if (e.key === 'Escape') handleCancelEdit()
}}
Expand Down
6 changes: 4 additions & 2 deletions packages/workshop-frontend/src/ShareModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
import { WorkshopButton, WorkshopIconButton } from './components/WorkshopControls'
import { PersonAvatar } from './components/PersonAvatar'
import { copyToClipboard } from './clipboard'
import { isComposingKeyEvent } from './utils/imeComposition'

type CollaboratorRow =
| { kind: 'owner'; profile: AiChatAuthorInfo }
Expand Down Expand Up @@ -808,7 +809,7 @@ export default function ShareModal({ open, onClose, overseer, metadata, currentU
aria-label="Username or email"
value={addUsername}
onChange={(e) => setAddUsername(e.target.value)}
onKeyDown={(e) => { if (e.key === 'Enter') handleAddCollaborator() }}
onKeyDown={(e) => { if (!isComposingKeyEvent(e) && e.key === 'Enter') handleAddCollaborator() }}
name="gadget-share-people-search"
autoComplete="off"
autoCorrect="off"
Expand Down Expand Up @@ -906,7 +907,7 @@ export default function ShareModal({ open, onClose, overseer, metadata, currentU
ref={linkNameRef}
value={newLinkNote}
onChange={(e) => setNewLinkNote(e.target.value)}
onKeyDown={(e) => { if (e.key === 'Enter') handleCreateShareLink() }}
onKeyDown={(e) => { if (!isComposingKeyEvent(e) && e.key === 'Enter') handleCreateShareLink() }}
placeholder="Name this link (optional)…"
aria-label="Share link name (optional)"
className="h-9 min-w-0 flex-1 border-0 bg-transparent p-0 text-[14px] leading-5 tracking-[-0.25px] text-kumo-default outline-none placeholder:text-kumo-inactive"
Expand Down Expand Up @@ -1040,6 +1041,7 @@ export default function ShareModal({ open, onClose, overseer, metadata, currentU
value={editingShareLinkNote}
onChange={(e) => setEditingShareLinkNote(e.target.value)}
onKeyDown={(e) => {
if (isComposingKeyEvent(e)) return
if (e.key === 'Enter') handleSaveShareLinkNote()
if (e.key === 'Escape') cancelRenameShareLink()
}}
Expand Down
2 changes: 2 additions & 0 deletions packages/workshop-frontend/src/WorkpiecePicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Tooltip } from '@cloudflare/kumo'
import type { WorkpieceId, WorkpieceSummary } from '@gadgets/workshop-shared/api'
import { CountBadge } from './components/CountBadge'
import { WorkshopIconButton, WorkshopInput } from './components/WorkshopControls'
import { isComposingKeyEvent } from './utils/imeComposition'

export const WORKPIECE_RAIL_COLLAPSED_WIDTH = 48
export const WORKPIECE_RAIL_EXPANDED_WIDTH = 220
Expand Down Expand Up @@ -88,6 +89,7 @@ export default function WorkpiecePicker({
value={editing.value}
onChange={e => setEditing({ id: gadget.id, value: e.target.value })}
onKeyDown={e => {
if (isComposingKeyEvent(e)) return
if (e.key === 'Enter') commitRename()
if (e.key === 'Escape') setEditing(null)
}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { useAuthenticatedApi } from '../../AuthContext'
import type { GadgetMetadataWithTimestamps, OutputFormatOffer } from '@gadgets/workshop-shared/api'
import { FormatGlyph } from '../format/FormatVisuals'
import { createFromFormat } from '../format/useOutputFormats'
import { isComposingKeyEvent } from '../../utils/imeComposition'

// A ⌘K command palette: jump to a workspace or a primary destination. Because it's keyboard-driven
// and opened many times a day, it deliberately has *no* open/close animation (instant feels faster
Expand Down Expand Up @@ -311,6 +312,8 @@ export default function CommandPalette({

const onKeyDown = useCallback(
(e: React.KeyboardEvent) => {
// Arrow keys pick IME candidates and Enter commits them; neither is ours mid-composition.
if (isComposingKeyEvent(e)) return
if (e.key === 'ArrowDown') {
e.preventDefault()
setActiveIndex((i) => (flat.length ? (i + 1) % flat.length : 0))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { DropdownMenu } from '@cloudflare/kumo'
import { MENU_CONTENT, MENU_ITEM, MENU_ITEM_DANGER, MENU_POSITIONER_STYLE } from '../menuStyles'
import { useState, useEffect, useRef } from 'react'
import type { GadgetMetadataWithTimestamps } from '@gadgets/workshop-shared/api'
import { isComposingKeyEvent } from '../../utils/imeComposition'

function initials(title: string | undefined): string {
const t = (title || 'Untitled').trim()
Expand Down Expand Up @@ -76,6 +77,7 @@ export default function SidebarGadgetRow({
onChange={(e) => setRenameValue(e.target.value)}
onBlur={commit}
onKeyDown={(e) => {
if (isComposingKeyEvent(e)) return
if (e.key === 'Enter') commit()
if (e.key === 'Escape') setRenaming(false)
}}
Expand Down
2 changes: 2 additions & 0 deletions packages/workshop-frontend/src/components/GadgetList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { BindingBadge, getGradient as getBlueprintGradient, uniqueBindingBadges
import { MENU_CONTENT, MENU_ITEM, MENU_ITEM_DANGER } from './menuStyles'
import { BlueprintPreviewImage } from './BlueprintPreviewImage'
import DeleteConfirmationDialog from './DeleteConfirmationDialog'
import { isComposingKeyEvent } from '../utils/imeComposition'

// Neutral monogram for a workspace — matches the sidebar treatment (no per-item color noise).
function initials(title: string | undefined): string {
Expand Down Expand Up @@ -96,6 +97,7 @@ function AppRow({
onChange={(e) => setRenameValue(e.target.value)}
onBlur={commitRename}
onKeyDown={(e) => {
if (isComposingKeyEvent(e)) return
if (e.key === 'Enter') commitRename()
if (e.key === 'Escape') setIsRenaming(false)
}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Dialog, Button, Input } from '@cloudflare/kumo'
import { X } from '@phosphor-icons/react'
import type { Connection, ConnectionResource } from '../../data/sample'
import { logoComponents } from '../ConnectionLogos'
import { isComposingKeyEvent } from '../../utils/imeComposition'

export default function ConnectionConfigModal({
connection,
Expand Down Expand Up @@ -80,6 +81,7 @@ export default function ConnectionConfigModal({
value={inputValue}
onChange={(e) => setInputValue(e.currentTarget.value)}
onKeyDown={(e) => {
if (isComposingKeyEvent(e)) return
if (e.key === 'Enter') handleAdd()
}}
placeholder={config.placeholder}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { useAuthenticatedApi } from '../../AuthContext'
import { MENU_CONTENT } from '../menuStyles'
import { FORMAT_ICONS, GENERIC_OUTPUT } from './formats'
import { FormatGlyph, FormatPreview } from './FormatVisuals'
import { isComposingKeyEvent } from '../../utils/imeComposition'

// A blueprint the admin could promote. `declared` is what it says it produces, when we know --
// known for the deployment's featured blueprints, unknown for the admin's own published ones.
Expand Down Expand Up @@ -501,6 +502,7 @@ function OverrideField({
onChange={(e) => setDraft(e.target.value)}
onBlur={commit}
onKeyDown={(e) => {
if (isComposingKeyEvent(e)) return
if (e.key === 'Enter') e.currentTarget.blur()
if (e.key === 'Escape') setDraft(value)
}}
Expand Down
14 changes: 14 additions & 0 deletions packages/workshop-frontend/src/utils/imeComposition.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type {KeyboardEvent as ReactKeyboardEvent} from 'react'

// True while an input method editor is composing text — e.g. the Enter that commits a Japanese
// kana-kanji conversion, or the arrow keys that pick a candidate. Those keystrokes belong to the
// IME, so handlers that treat Enter as "submit" must ignore them, or the first conversion in a
// message sends it half-written.
//
// `isComposing` covers modern browsers; keyCode 229 is the legacy signal some IMEs still emit for
// keydown during composition (notably Safari, where `isComposing` can be false on the commit key).
export function isComposingKeyEvent(
event: ReactKeyboardEvent | KeyboardEvent): boolean {
const native = 'nativeEvent' in event ? event.nativeEvent : event
return native.isComposing || native.keyCode === 229
}
Loading