From 8190a618a1585b4c87a0ed076e6578230bb55980 Mon Sep 17 00:00:00 2001 From: Evgeny Shurakov Date: Sun, 10 May 2026 22:02:21 +0200 Subject: [PATCH 1/6] feat(cloud-agent-next): add slash command support with discriminated payloads Add discriminated send payloads (prompt vs command) across the cloud agent stack: - Introduce ExecutionPayload union (PromptExecutionPayload | CommandExecutionPayload) in cloud-agent-next execution types. - Add dispatchToWrapper() to route prompts vs commands to the wrapper. - Add commands.available event to cache and broadcast kilo slash commands. - Update web SDK transport, session manager, and UI to handle command payloads and available commands. - Update mobile session manager and detail content for slash commands. - Remove hardcoded default-command-sets in favor of runtime catalog. --- .../agents/mobile-session-manager.ts | 51 +- .../agents/session-detail-content.tsx | 11 +- .../components/cloud-agent-next/ChatInput.tsx | 153 +- .../cloud-agent-next/CloudAgentProvider.tsx | 50 +- .../cloud-agent-next/CloudChatPage.tsx | 34 +- .../cloud-agent-next/NewSessionPanel.tsx | 173 +- .../cloud-agent/ProfilePickerPopover.tsx | 9 +- .../cloud-agent/ProfilesListDialog.tsx | 48 +- .../profile-editor/KiloCommandsTab.tsx | 403 + apps/web/src/hooks/useCloudAgentProfiles.ts | 98 + .../src/hooks/useSlashCommandAutocomplete.ts | 128 + apps/web/src/hooks/useSlashCommandSets.ts | 53 +- .../lib/app-builder/app-builder-service.ts | 9 +- .../cloud-agent-next/cloud-agent-client.ts | 25 +- .../cli-live-transport.test.ts | 20 +- .../lib/cloud-agent-sdk/cli-live-transport.ts | 27 +- .../cloud-agent-transport.test.ts | 8 +- apps/web/src/lib/cloud-agent-sdk/index.ts | 4 + .../lib/cloud-agent-sdk/normalizer.test.ts | 34 + .../web/src/lib/cloud-agent-sdk/normalizer.ts | 12 +- apps/web/src/lib/cloud-agent-sdk/schemas.ts | 20 + .../cloud-agent-sdk/session-manager.test.ts | 117 +- .../lib/cloud-agent-sdk/session-manager.ts | 60 +- .../cloud-agent-sdk/session-transport.test.ts | 13 +- apps/web/src/lib/cloud-agent-sdk/session.ts | 6 +- apps/web/src/lib/cloud-agent-sdk/transport.ts | 34 +- apps/web/src/lib/cloud-agent-sdk/types.ts | 15 + .../src/lib/cloud-agent/cloud-agent-client.ts | 13 +- .../lib/cloud-agent/default-command-sets.ts | 40 - apps/web/src/routers/agent-profiles-router.ts | 144 + .../src/routers/cloud-agent-next-schemas.ts | 33 +- apps/web/src/routers/cloud-agent-schemas.ts | 33 +- .../organization-cloud-agent-next-router.ts | 5 +- apps/web/tsconfig.json | 4 +- packages/cloud-agent-profile/src/index.ts | 24 + .../src/profile-kilo-commands-service.test.ts | 147 + .../src/profile-kilo-commands-service.ts | 273 + .../src/profile-service.ts | 101 +- .../src/profile-session-config.ts | 25 +- packages/cloud-agent-profile/src/types.ts | 17 + .../src/migrations/0118_charming_luckman.sql | 18 + .../db/src/migrations/meta/0117_snapshot.json | 19159 ---------------- .../db/src/migrations/meta/0118_snapshot.json | 4558 +++- packages/db/src/migrations/meta/_journal.json | 43 +- packages/db/src/schema.ts | 37 + pnpm-lock.yaml | 16 +- services/cloud-agent-next/Dockerfile | 2 +- services/cloud-agent-next/Dockerfile.dev | 2 +- services/cloud-agent-next/package.json | 3 +- .../scripts/update-default-slash-commands.mjs | 235 + .../src/execution/orchestrator.ts | 32 +- .../cloud-agent-next/src/execution/types.ts | 55 +- .../src/execution/wrapper-call.test.ts | 114 + .../src/execution/wrapper-call.ts | 87 + .../src/kilo/wrapper-client.ts | 3 + .../src/persistence/CloudAgentSession.ts | 85 +- .../src/persistence/schemas.ts | 54 + .../cloud-agent-next/src/persistence/types.ts | 16 +- .../src/router/handlers/session-execution.ts | 5 +- .../src/router/handlers/session-prepare.ts | 9 + .../cloud-agent-next/src/router/schemas.ts | 57 +- services/cloud-agent-next/src/schema.ts | 4 + .../cloud-agent-next/src/session-profile.ts | 3 + .../src/session-service.test.ts | 162 + .../cloud-agent-next/src/session-service.ts | 27 +- .../commands-available.test.ts | 76 + .../ingest-handlers/commands-available.ts | 45 + .../src/session/ingest-handlers/index.ts | 1 + .../default-slash-commands.generated.ts | 42 + .../cloud-agent-next/src/shared/protocol.ts | 15 +- .../src/shared/slash-commands.test.ts | 122 + .../src/shared/slash-commands.ts | 80 + .../src/websocket/ingest.test.ts | 1 + .../cloud-agent-next/src/websocket/ingest.ts | 12 + .../src/websocket/stream.test.ts | 121 +- .../cloud-agent-next/src/websocket/stream.ts | 41 +- .../cloud-agent-next/src/websocket/types.ts | 3 +- .../session/execute-directly-failure.test.ts | 2 +- .../session/start-execution-v2.test.ts | 2 +- services/cloud-agent-next/wrangler.jsonc | 8 +- .../cloud-agent-next/wrapper/package.json | 2 +- .../wrapper/src/connection.ts | 28 + .../cloud-agent-next/wrapper/src/kilo-api.ts | 22 +- .../cloud-agent-next/wrapper/src/server.ts | 32 +- 84 files changed, 7032 insertions(+), 20883 deletions(-) create mode 100644 apps/web/src/components/cloud-agent/profile-editor/KiloCommandsTab.tsx create mode 100644 apps/web/src/hooks/useSlashCommandAutocomplete.ts delete mode 100644 apps/web/src/lib/cloud-agent/default-command-sets.ts create mode 100644 packages/cloud-agent-profile/src/profile-kilo-commands-service.test.ts create mode 100644 packages/cloud-agent-profile/src/profile-kilo-commands-service.ts create mode 100644 packages/db/src/migrations/0118_charming_luckman.sql delete mode 100644 packages/db/src/migrations/meta/0117_snapshot.json create mode 100755 services/cloud-agent-next/scripts/update-default-slash-commands.mjs create mode 100644 services/cloud-agent-next/src/execution/wrapper-call.test.ts create mode 100644 services/cloud-agent-next/src/execution/wrapper-call.ts create mode 100644 services/cloud-agent-next/src/session/ingest-handlers/commands-available.test.ts create mode 100644 services/cloud-agent-next/src/session/ingest-handlers/commands-available.ts create mode 100644 services/cloud-agent-next/src/shared/default-slash-commands.generated.ts create mode 100644 services/cloud-agent-next/src/shared/slash-commands.test.ts create mode 100644 services/cloud-agent-next/src/shared/slash-commands.ts diff --git a/apps/mobile/src/components/agents/mobile-session-manager.ts b/apps/mobile/src/components/agents/mobile-session-manager.ts index 42e05630d0..e65507b968 100644 --- a/apps/mobile/src/components/agents/mobile-session-manager.ts +++ b/apps/mobile/src/components/agents/mobile-session-manager.ts @@ -114,28 +114,53 @@ export function createMobileAgentSessionManager({ return result.token; }, api: { - send: async payload => { + send: async input => { await withCloudAgentDiagnostics('send', organizationId, async () => { - if (!payload.model) { - throw new Error('Model is required'); - } - const input = { - cloudAgentSessionId: payload.sessionId as string, - prompt: payload.prompt, - mode: normalizeAgentMode(payload.mode), - model: payload.model, - variant: payload.variant, + // The cloud-agent-next SDK transport now passes a discriminated payload. + // For prompt payloads we still require mode/model client-side; commands + // carry their own agent/model overrides on the kilo side. + type WorkerPayload = + | { + type: 'prompt'; + prompt: string; + mode: string; + model: string; + variant?: string; + } + | { type: 'command'; command: string; arguments: string }; + const payload: WorkerPayload = (() => { + if (input.payload.type === 'prompt') { + if (!input.payload.model) { + throw new Error('Model is required'); + } + return { + type: 'prompt' as const, + prompt: input.payload.prompt, + mode: normalizeAgentMode(input.payload.mode), + model: input.payload.model, + variant: input.payload.variant, + }; + } + return { + type: 'command' as const, + command: input.payload.command, + arguments: input.payload.arguments, + }; + })(); + const baseInput = { + cloudAgentSessionId: input.sessionId as string, + payload, autoCommit: true, - messageId: payload.messageId, + messageId: input.messageId, }; if (organizationId) { await trpcClient.organizations.cloudAgentNext.sendMessage.mutate( - { ...input, organizationId }, + { ...baseInput, organizationId }, skipBatchOptions ); return; } - await trpcClient.cloudAgentNext.sendMessage.mutate(input, skipBatchOptions); + await trpcClient.cloudAgentNext.sendMessage.mutate(baseInput, skipBatchOptions); }); }, interrupt: async payload => { diff --git a/apps/mobile/src/components/agents/session-detail-content.tsx b/apps/mobile/src/components/agents/session-detail-content.tsx index 1435dc6d26..d29e18aca8 100644 --- a/apps/mobile/src/components/agents/session-detail-content.tsx +++ b/apps/mobile/src/components/agents/session-detail-content.tsx @@ -190,10 +190,13 @@ export function SessionDetailContent({ sessionId }: Readonly Promise; + /** + * Invoked when the user submits a slash command (input starts with `/` + * and `` matches a known entry in `slashCommands`). When omitted or + * the input doesn't match a known command, the input is forwarded to + * `onSend` as plain text instead. + */ + onSendCommand?: (command: string, args: string, images?: Images) => Promise; onStop?: () => void; disabled?: boolean; isStreaming?: boolean; @@ -71,6 +79,7 @@ type ChatInputProps = { export function ChatInput({ onSend, + onSendCommand, onStop, disabled = false, isStreaming = false, @@ -95,11 +104,10 @@ export function ChatInput({ variantPickerTooltip, }: ChatInputProps) { const [value, setValue] = useState(''); - const [showAutocomplete, setShowAutocomplete] = useState(false); - const [selectedIndex, setSelectedIndex] = useState(0); const valueRef = useRef(''); const textareaRef = useRef(null); const fileInputRef = useRef(null); + const commandListRef = useRef(null); const setInputValue = useCallback((nextValue: string) => { valueRef.current = nextValue; @@ -132,34 +140,6 @@ export function ChatInput({ ? formatShortModelDisplayName(lockedModelOption.name) : model; - // Filter commands based on current input - const filteredCommands = useMemo(() => { - if (!slashCommands || slashCommands.length === 0) return []; - if (!value.startsWith('/')) return []; - - const query = value.slice(1).toLowerCase(); - return slashCommands.filter(cmd => cmd.trigger.toLowerCase().startsWith(query)); - }, [value, slashCommands]); - - // Determine if autocomplete should be shown - const shouldShowAutocomplete = useMemo(() => { - return ( - value.startsWith('/') && - filteredCommands.length > 0 && - slashCommands && - slashCommands.length > 0 - ); - }, [value, filteredCommands.length, slashCommands]); - - // Update showAutocomplete state when conditions change - useEffect(() => { - setShowAutocomplete(shouldShowAutocomplete); - // Reset selected index when filtering changes - if (shouldShowAutocomplete) { - setSelectedIndex(0); - } - }, [shouldShowAutocomplete]); - useEffect(() => { const textarea = textareaRef.current; if (!textarea) return; @@ -180,13 +160,27 @@ export function ChatInput({ setInputValue(''); submittedImageIds.forEach(imageUpload.removeImage); - setShowAutocomplete(false); if (textareaRef.current) { textareaRef.current.style.height = 'auto'; } - const accepted = await onSend(trimmed, imagesData); + // Re-match against the trimmed value at submit time + let accepted: boolean; + const slashMatch = onSendCommand + ? /^\s*\/([\w.-]+)(?:\s+([\s\S]*))?\s*$/.exec(trimmed) + : null; + const slashCommand = + slashMatch && slashCommands?.some(c => c.trigger === slashMatch[1]) + ? { command: slashMatch[1], args: slashMatch[2]?.trim() ?? '' } + : null; + + if (slashCommand && onSendCommand) { + accepted = await onSendCommand(slashCommand.command, slashCommand.args, imagesData); + } else { + accepted = await onSend(trimmed, imagesData); + } + if (!accepted) { if (valueRef.current === '') { setInputValue(trimmed); @@ -197,7 +191,7 @@ export function ChatInput({ return true; }, - [disabled, imageUpload, onSend, setInputValue] + [disabled, imageUpload, onSend, onSendCommand, setInputValue, slashCommands] ); const handleSend = () => { @@ -210,63 +204,44 @@ export function ChatInput({ } }; - const handleSelectCommand = (command: SlashCommand, autoSend = false) => { - const expansion = command.expansion; - setShowAutocomplete(false); - setSelectedIndex(0); - - if (autoSend) { - void sendMessage(expansion); - } else { - // Just fill the input for editing - setInputValue(expansion); - // Force height recalculation for expanded text - if (textareaRef.current) { - textareaRef.current.style.height = 'auto'; - textareaRef.current.style.height = `${Math.min(textareaRef.current.scrollHeight, 200)}px`; + const handleSelectCommand = useCallback( + (command: SlashCommand, autoSend = false) => { + if (autoSend) { + void sendMessage(`/${command.trigger}`); + } else { + const inserted = `/${command.trigger} `; + setInputValue(inserted); + if (textareaRef.current) { + textareaRef.current.style.height = 'auto'; + textareaRef.current.style.height = `${Math.min(textareaRef.current.scrollHeight, 200)}px`; + const end = inserted.length; + textareaRef.current.setSelectionRange(end, end); + } } - } - // Keep focus on textarea - textareaRef.current?.focus(); - }; + textareaRef.current?.focus(); + }, + [sendMessage, setInputValue] + ); + + const { + showAutocomplete, + selectedIndex, + setSelectedIndex, + filteredCommands, + handleKeyDown: handleAutocompleteKeyDown, + setShowAutocomplete, + } = useSlashCommandAutocomplete({ + value, + slashCommands, + onSelect: handleSelectCommand, + listRef: commandListRef, + }); const handleKeyDown = (e: KeyboardEvent) => { - // Ignore keyboard events during IME composition (Chinese, Japanese, Korean input) if (e.nativeEvent.isComposing || e.nativeEvent.keyCode === 229) return; - if (showAutocomplete && filteredCommands.length > 0) { - switch (e.key) { - case 'ArrowDown': - e.preventDefault(); - setSelectedIndex(prev => (prev + 1) % filteredCommands.length); - return; - case 'ArrowUp': - e.preventDefault(); - setSelectedIndex(prev => (prev - 1 + filteredCommands.length) % filteredCommands.length); - return; - case 'Enter': - e.preventDefault(); - // Bounds check to prevent race condition - if (selectedIndex >= 0 && selectedIndex < filteredCommands.length) { - // Enter = select and send; Shift+Enter = select and expand only - handleSelectCommand(filteredCommands[selectedIndex], !e.shiftKey); - } - return; - case 'Tab': - e.preventDefault(); - // Bounds check to prevent race condition - if (selectedIndex >= 0 && selectedIndex < filteredCommands.length) { - // Tab = select and expand only (don't send) - handleSelectCommand(filteredCommands[selectedIndex], false); - } - return; - case 'Escape': - e.preventDefault(); - setShowAutocomplete(false); - return; - } - } + if (handleAutocompleteKeyDown(e)) return; if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); @@ -368,8 +343,9 @@ export function ChatInput({ sideOffset={4} onOpenAutoFocus={e => e.preventDefault()} > - + handleSelectCommand(cmd)} - className={cn( - 'flex cursor-pointer flex-col items-start gap-1 px-3 py-2', - index === selectedIndex && 'bg-accent' - )} + className="flex cursor-pointer flex-col items-start gap-1 px-3 py-2" onMouseEnter={() => setSelectedIndex(index)} - role="option" - aria-selected={index === selectedIndex} >
diff --git a/apps/web/src/components/cloud-agent-next/CloudAgentProvider.tsx b/apps/web/src/components/cloud-agent-next/CloudAgentProvider.tsx index 760a19e7c7..93fc5622ed 100644 --- a/apps/web/src/components/cloud-agent-next/CloudAgentProvider.tsx +++ b/apps/web/src/components/cloud-agent-next/CloudAgentProvider.tsx @@ -13,6 +13,7 @@ import { type KiloSessionId, type CloudAgentSessionId, } from '@/lib/cloud-agent-sdk'; +import type { SendMessagePayload } from '@/lib/cloud-agent-next/cloud-agent-client'; import { CLOUD_AGENT_NEXT_WS_URL, SESSION_INGEST_WS_URL } from '@/lib/constants'; import { usePostHog } from 'posthog-js/react'; @@ -109,37 +110,50 @@ export function CloudAgentProvider({ children, organizationId }: CloudAgentProvi lifecycleHooks: createBrowserLifecycleHooks(), api: { - send: async payload => { - const mode = payload.mode ?? 'code'; - if (payload.model === undefined) { - throw new Error('Cloud Agent model is required'); + send: async input => { + // The transport-level SendPromptPayload makes mode/model optional (CLI + // live transport accepts them as optional). The cloud-agent worker + // schema requires them for prompts, so coerce here and fail loudly + // if missing. + let normalizedPayload: SendMessagePayload; + if (input.payload.type === 'prompt') { + if (!input.payload.mode) throw new Error('Cloud Agent mode is required'); + if (!input.payload.model) throw new Error('Cloud Agent model is required'); + normalizedPayload = { + type: 'prompt', + prompt: input.payload.prompt, + mode: input.payload.mode, + model: input.payload.model, + variant: input.payload.variant, + }; + } else { + normalizedPayload = { + type: 'command', + command: input.payload.command, + arguments: input.payload.arguments, + }; } + if (organizationId) { return trpcClient.organizations.cloudAgentNext.sendMessage.mutate( { - cloudAgentSessionId: payload.sessionId, - prompt: payload.prompt, - mode, - model: payload.model, - variant: payload.variant, + cloudAgentSessionId: input.sessionId, + payload: normalizedPayload, autoCommit: true, organizationId, - messageId: payload.messageId, - images: payload.images, + messageId: input.messageId, + images: input.images, }, { context: { skipBatch: true } } ); } return trpcClient.cloudAgentNext.sendMessage.mutate( { - cloudAgentSessionId: payload.sessionId, - prompt: payload.prompt, - mode, - model: payload.model, - variant: payload.variant, + cloudAgentSessionId: input.sessionId, + payload: normalizedPayload, autoCommit: true, - messageId: payload.messageId, - images: payload.images, + messageId: input.messageId, + images: input.images, }, { context: { skipBatch: true } } ); diff --git a/apps/web/src/components/cloud-agent-next/CloudChatPage.tsx b/apps/web/src/components/cloud-agent-next/CloudChatPage.tsx index 7a28db32f8..0304f09e5d 100644 --- a/apps/web/src/components/cloud-agent-next/CloudChatPage.tsx +++ b/apps/web/src/components/cloud-agent-next/CloudChatPage.tsx @@ -288,12 +288,15 @@ export default function CloudChatPage({ organizationId }: CloudChatPageProps) { ? selectedRuntimeAgentForSend?.variant?.trim() || undefined : undefined; const acceptedPromise = manager.send({ - prompt, - mode: sessionConfig?.mode ?? 'code', - model: agentModelOverrideForSend ?? sessionConfig?.model ?? '', - variant: agentModelOverrideForSend - ? agentVariantOverrideForSend - : (sessionConfig?.variant ?? undefined), + payload: { + type: 'prompt', + prompt, + mode: sessionConfig?.mode ?? 'code', + model: agentModelOverrideForSend ?? sessionConfig?.model ?? '', + variant: agentModelOverrideForSend + ? agentVariantOverrideForSend + : (sessionConfig?.variant ?? undefined), + }, images, }); scheduleScrollToBottom(); @@ -308,6 +311,24 @@ export default function CloudChatPage({ organizationId }: CloudChatPageProps) { [manager, scheduleScrollToBottom, sessionConfig, setChatUI] ); + const handleSendSlashCommand = useCallback( + async (command: string, args: string, images?: Images) => { + setChatUI({ shouldAutoScroll: true }); + const acceptedPromise = manager.send({ + payload: { type: 'command', command, arguments: args }, + images, + }); + scheduleScrollToBottom(); + const accepted = await acceptedPromise; + if (accepted) { + setImageMessageUuid(crypto.randomUUID()); + scheduleScrollToBottom(); + } + return accepted; + }, + [manager, scheduleScrollToBottom, setChatUI] + ); + const handleStopExecution = useCallback(() => { void manager.interrupt(); }, [manager]); @@ -555,6 +576,7 @@ export default function CloudChatPage({ organizationId }: CloudChatPageProps) {
(null); const fileInputRef = useRef(null); + const commandListRef = useRef(null); const { mutateAsync: personalUploadUrl } = useMutation( trpc.cloudAgentNext.getImageUploadUrl.mutationOptions() ); @@ -565,6 +570,57 @@ export function NewSessionPanel({ organizationId }: NewSessionPanelProps) { [handleRepoSelect] ); + // --------------------------------------------------------------------------- + // Slash commands + // --------------------------------------------------------------------------- + const slashCommands = useMemo(() => { + const defaults = commandsOrDefault(undefined).map(cmd => ({ + trigger: cmd.name, + label: cmd.name, + description: cmd.description ?? '', + expansion: '', + })); + const profileCommands = (selectedProfileDetails?.kiloCommands ?? []) + .filter(cmd => cmd.enabled) + .map(cmd => ({ + trigger: cmd.name, + label: cmd.name, + description: cmd.description ?? '', + expansion: '', + })); + return [...defaults, ...profileCommands]; + }, [selectedProfileDetails?.kiloCommands]); + + const handleSelectCommand = useCallback((command: SlashCommand, autoSend = false) => { + if (autoSend) { + setPrompt(`/${command.trigger}`); + } else { + const inserted = `/${command.trigger} `; + setPrompt(inserted); + if (textareaRef.current) { + textareaRef.current.style.height = 'auto'; + textareaRef.current.style.height = `${Math.min(textareaRef.current.scrollHeight, window.innerHeight * 0.5)}px`; + const end = inserted.length; + textareaRef.current.setSelectionRange(end, end); + } + } + textareaRef.current?.focus(); + }, []); + + const { + showAutocomplete, + selectedIndex, + setSelectedIndex, + filteredCommands, + handleKeyDown: handleAutocompleteKeyDown, + setShowAutocomplete, + } = useSlashCommandAutocomplete({ + value: prompt, + slashCommands, + onSelect: handleSelectCommand, + listRef: commandListRef, + }); + // --------------------------------------------------------------------------- // Submit // --------------------------------------------------------------------------- @@ -589,8 +645,27 @@ export function NewSessionPanel({ organizationId }: NewSessionPanelProps) { try { const initialMessageId = generateMessageId(); + const trimmed = prompt.trim(); + + // Parse slash command: if the input matches a known command, send a + // structured initialPayload so the backend dispatches a command rather + // than treating the text as a free-text prompt. + const slashMatch = /^\s*\/([\w.-]+)(?:\s+([\s\S]*))?\s*$/.exec(trimmed); + const slashCommand = + slashMatch && slashCommands.some(c => c.trigger === slashMatch[1]) + ? { command: slashMatch[1], args: slashMatch[2]?.trim() ?? '' } + : null; + + if (slashCommand && imageUpload.images.length > 0) { + toast.error('Images cannot be attached to slash commands', { + description: 'Remove the images or type a plain prompt instead.', + }); + setIsPreparing(false); + return; + } + const baseInput = { - prompt: prompt.trim(), + prompt: trimmed, mode, model: displayModel, variant: displayVariant, @@ -599,6 +674,15 @@ export function NewSessionPanel({ organizationId }: NewSessionPanelProps) { autoInitiate: true, initialMessageId, images: imageUpload.getImagesData(), + ...(slashCommand + ? { + initialPayload: { + type: 'command' as const, + command: slashCommand.command, + arguments: slashCommand.args, + }, + } + : {}), }; let result: { kiloSessionId: string; cloudAgentSessionId: string }; @@ -677,6 +761,7 @@ export function NewSessionPanel({ organizationId }: NewSessionPanelProps) { selectedPlatform, selectedProfileId, selectedRepo, + slashCommands, trpc.cliSessionsV2.list, trpcClient, ]); @@ -717,6 +802,10 @@ export function NewSessionPanel({ organizationId }: NewSessionPanelProps) { const handleKeyDown = useCallback( (e: React.KeyboardEvent) => { + if (e.nativeEvent.isComposing || e.nativeEvent.keyCode === 229) return; + + if (handleAutocompleteKeyDown(e)) return; + if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) { e.preventDefault(); if (isFormValid) { @@ -724,7 +813,7 @@ export function NewSessionPanel({ organizationId }: NewSessionPanelProps) { } } }, - [isFormValid, handleStartSession] + [handleAutocompleteKeyDown, isFormValid, handleStartSession] ); // --------------------------------------------------------------------------- @@ -827,18 +916,60 @@ export function NewSessionPanel({ organizationId }: NewSessionPanelProps) { } }} /> -