diff --git a/apps/backend/internal/user/controller/controller.go b/apps/backend/internal/user/controller/controller.go index 00a11dece7..0be0329266 100644 --- a/apps/backend/internal/user/controller/controller.go +++ b/apps/backend/internal/user/controller/controller.go @@ -51,6 +51,7 @@ func (c *Controller) UpdateUserSettings(ctx context.Context, req dto.UpdateUserS PreferredShell: req.PreferredShell, DefaultEditorID: req.DefaultEditorID, EnablePreviewOnClick: req.EnablePreviewOnClick, + ChatSubmitKey: req.ChatSubmitKey, }) if err != nil { return dto.UserSettingsResponse{}, err diff --git a/apps/backend/internal/user/dto/dto.go b/apps/backend/internal/user/dto/dto.go index f286afc219..c0d9c519e7 100644 --- a/apps/backend/internal/user/dto/dto.go +++ b/apps/backend/internal/user/dto/dto.go @@ -22,6 +22,7 @@ type UserSettingsDTO struct { PreferredShell string `json:"preferred_shell"` DefaultEditorID string `json:"default_editor_id"` EnablePreviewOnClick bool `json:"enable_preview_on_click"` + ChatSubmitKey string `json:"chat_submit_key"` UpdatedAt string `json:"updated_at"` } @@ -48,6 +49,7 @@ type UpdateUserSettingsRequest struct { PreferredShell *string `json:"preferred_shell,omitempty"` DefaultEditorID *string `json:"default_editor_id,omitempty"` EnablePreviewOnClick *bool `json:"enable_preview_on_click,omitempty"` + ChatSubmitKey *string `json:"chat_submit_key,omitempty"` } func FromUser(user *models.User) UserDTO { @@ -69,6 +71,7 @@ func FromUserSettings(settings *models.UserSettings) UserSettingsDTO { PreferredShell: settings.PreferredShell, DefaultEditorID: settings.DefaultEditorID, EnablePreviewOnClick: settings.EnablePreviewOnClick, + ChatSubmitKey: settings.ChatSubmitKey, UpdatedAt: settings.UpdatedAt.Format(time.RFC3339), } } diff --git a/apps/backend/internal/user/handlers/handlers.go b/apps/backend/internal/user/handlers/handlers.go index a2dc45918b..bb156fe3cb 100644 --- a/apps/backend/internal/user/handlers/handlers.go +++ b/apps/backend/internal/user/handlers/handlers.go @@ -71,6 +71,7 @@ type httpUpdateUserSettingsRequest struct { PreferredShell *string `json:"preferred_shell,omitempty"` DefaultEditorID *string `json:"default_editor_id,omitempty"` EnablePreviewOnClick *bool `json:"enable_preview_on_click,omitempty"` + ChatSubmitKey *string `json:"chat_submit_key,omitempty"` } func (h *Handlers) httpUpdateUserSettings(c *gin.Context) { @@ -87,6 +88,7 @@ func (h *Handlers) httpUpdateUserSettings(c *gin.Context) { PreferredShell: body.PreferredShell, DefaultEditorID: body.DefaultEditorID, EnablePreviewOnClick: body.EnablePreviewOnClick, + ChatSubmitKey: body.ChatSubmitKey, }) if err != nil { h.logger.Error("failed to update user settings", zap.Error(err)) @@ -112,6 +114,7 @@ type wsUpdateUserSettingsRequest struct { PreferredShell *string `json:"preferred_shell,omitempty"` DefaultEditorID *string `json:"default_editor_id,omitempty"` EnablePreviewOnClick *bool `json:"enable_preview_on_click,omitempty"` + ChatSubmitKey *string `json:"chat_submit_key,omitempty"` } func (h *Handlers) wsUpdateUserSettings(ctx context.Context, msg *ws.Message) (*ws.Message, error) { @@ -127,6 +130,7 @@ func (h *Handlers) wsUpdateUserSettings(ctx context.Context, msg *ws.Message) (* PreferredShell: req.PreferredShell, DefaultEditorID: req.DefaultEditorID, EnablePreviewOnClick: req.EnablePreviewOnClick, + ChatSubmitKey: req.ChatSubmitKey, }) if err != nil { return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeInternalError, "Failed to update user settings", nil) diff --git a/apps/backend/internal/user/models/models.go b/apps/backend/internal/user/models/models.go index fb71fb749c..783b07acda 100644 --- a/apps/backend/internal/user/models/models.go +++ b/apps/backend/internal/user/models/models.go @@ -18,6 +18,7 @@ type UserSettings struct { PreferredShell string `json:"preferred_shell"` DefaultEditorID string `json:"default_editor_id"` EnablePreviewOnClick bool `json:"enable_preview_on_click"` + ChatSubmitKey string `json:"chat_submit_key"` // "enter" | "cmd_enter" CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } diff --git a/apps/backend/internal/user/service/service.go b/apps/backend/internal/user/service/service.go index 6cd4428df3..e1d9b848ee 100644 --- a/apps/backend/internal/user/service/service.go +++ b/apps/backend/internal/user/service/service.go @@ -31,6 +31,7 @@ type UpdateUserSettingsRequest struct { PreferredShell *string DefaultEditorID *string EnablePreviewOnClick *bool + ChatSubmitKey *string } func NewService(repo store.Repository, eventBus bus.EventBus, log *logger.Logger) *Service { @@ -92,6 +93,14 @@ func (s *Service) UpdateUserSettings(ctx context.Context, req *UpdateUserSetting if req.EnablePreviewOnClick != nil { settings.EnablePreviewOnClick = *req.EnablePreviewOnClick } + if req.ChatSubmitKey != nil { + key := strings.TrimSpace(*req.ChatSubmitKey) + if key != "enter" && key != "cmd_enter" { + return nil, errors.New("chat_submit_key must be 'enter' or 'cmd_enter'") + } + s.logger.Info("[Settings] Setting ChatSubmitKey", zap.String("value", key)) + settings.ChatSubmitKey = key + } settings.UpdatedAt = time.Now().UTC() if err := s.repo.UpsertUserSettings(ctx, settings); err != nil { return nil, err @@ -113,6 +122,7 @@ func (s *Service) publishUserSettingsEvent(ctx context.Context, settings *models "preferred_shell": settings.PreferredShell, "default_editor_id": settings.DefaultEditorID, "enable_preview_on_click": settings.EnablePreviewOnClick, + "chat_submit_key": settings.ChatSubmitKey, "updated_at": settings.UpdatedAt.Format(time.RFC3339), } if err := s.eventBus.Publish(ctx, events.UserSettingsUpdated, bus.NewEvent(events.UserSettingsUpdated, "user-service", data)); err != nil { diff --git a/apps/backend/internal/user/store/sqlite.go b/apps/backend/internal/user/store/sqlite.go index b5295c4640..53cba4d2cd 100644 --- a/apps/backend/internal/user/store/sqlite.go +++ b/apps/backend/internal/user/store/sqlite.go @@ -127,6 +127,7 @@ func (r *sqliteRepository) UpsertUserSettings(ctx context.Context, settings *mod "preferred_shell": settings.PreferredShell, "default_editor_id": settings.DefaultEditorID, "enable_preview_on_click": settings.EnablePreviewOnClick, + "chat_submit_key": settings.ChatSubmitKey, }) if err != nil { return err @@ -172,6 +173,7 @@ func scanUserSettings(scanner interface{ Scan(dest ...any) error }, userID strin PreferredShell string `json:"preferred_shell"` DefaultEditorID string `json:"default_editor_id"` EnablePreviewOnClick bool `json:"enable_preview_on_click"` + ChatSubmitKey string `json:"chat_submit_key"` } if err := json.Unmarshal([]byte(settingsRaw), &payload); err != nil { return nil, err @@ -183,5 +185,10 @@ func scanUserSettings(scanner interface{ Scan(dest ...any) error }, userID strin settings.PreferredShell = payload.PreferredShell settings.DefaultEditorID = payload.DefaultEditorID settings.EnablePreviewOnClick = payload.EnablePreviewOnClick + settings.ChatSubmitKey = payload.ChatSubmitKey + // Default to "cmd_enter" if empty + if settings.ChatSubmitKey == "" { + settings.ChatSubmitKey = "cmd_enter" + } return settings, nil } diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx index 0965606220..968743b1c1 100644 --- a/apps/web/app/page.tsx +++ b/apps/web/app/page.tsx @@ -58,6 +58,7 @@ export default async function Page({ searchParams }: PageProps) { shellOptions: userSettingsResponse?.shell_options ?? [], defaultEditorId: userSettings?.default_editor_id || null, enablePreviewOnClick: settingsEnablePreviewOnClick, + chatSubmitKey: userSettings?.chat_submit_key ?? 'cmd_enter', loaded: Boolean(userSettings), }, }; diff --git a/apps/web/app/s/[sessionId]/page.tsx b/apps/web/app/s/[sessionId]/page.tsx index 5e5b1b5020..e9fef3daf2 100644 --- a/apps/web/app/s/[sessionId]/page.tsx +++ b/apps/web/app/s/[sessionId]/page.tsx @@ -4,6 +4,7 @@ import { fetchBoardSnapshot, fetchTaskSession, fetchTask, + fetchUserSettings, listAgents, listAvailableAgents, listBoards, @@ -38,7 +39,7 @@ export default async function SessionPage({ } task = await fetchTask(session.task_id, { cache: 'no-store' }); - const [snapshot, agents, repositoriesResponse, allSessionsResponse, availableAgentsResponse, workspacesResponse, boardsResponse, turnsResponse] = await Promise.all([ + const [snapshot, agents, repositoriesResponse, allSessionsResponse, availableAgentsResponse, workspacesResponse, boardsResponse, turnsResponse, userSettingsResponse] = await Promise.all([ fetchBoardSnapshot(task.board_id, { cache: 'no-store' }), listAgents({ cache: 'no-store' }), listRepositories(task.workspace_id, { includeScripts: true }, { cache: 'no-store' }), @@ -47,6 +48,7 @@ export default async function SessionPage({ listWorkspaces({ cache: 'no-store' }).catch(() => ({ workspaces: [] })), listBoards(task.workspace_id, { cache: 'no-store' }).catch(() => ({ boards: [] })), listSessionTurns(sessionId, { cache: 'no-store' }).catch(() => ({ turns: [], total: 0 })), + fetchUserSettings({ cache: 'no-store' }).catch(() => null), ]); const repositories = repositoriesResponse.repositories ?? []; const allSessions = allSessionsResponse.sessions ?? [session]; @@ -54,6 +56,7 @@ export default async function SessionPage({ const workspaces = workspacesResponse.workspaces ?? []; const boards = boardsResponse.boards ?? []; const turns = turnsResponse.turns ?? []; + const userSettings = userSettingsResponse?.settings; // Get repository scripts from the repository (already fetched with includeScripts) const repositoryId = task.repositories?.[0]?.repository_id; @@ -203,6 +206,17 @@ export default async function SessionPage({ loaded: true, loading: false, }, + userSettings: { + workspaceId: userSettings?.workspace_id || null, + boardId: userSettings?.board_id || null, + repositoryIds: Array.from(new Set(userSettings?.repository_ids ?? [])).sort(), + preferredShell: userSettings?.preferred_shell || null, + shellOptions: userSettingsResponse?.shell_options ?? [], + defaultEditorId: userSettings?.default_editor_id || null, + enablePreviewOnClick: userSettings?.enable_preview_on_click ?? false, + chatSubmitKey: userSettings?.chat_submit_key ?? 'cmd_enter', + loaded: Boolean(userSettings), + }, }; } catch (error) { console.warn( diff --git a/apps/web/app/settings/general/page.tsx b/apps/web/app/settings/general/page.tsx index 949371d188..e614baf132 100644 --- a/apps/web/app/settings/general/page.tsx +++ b/apps/web/app/settings/general/page.tsx @@ -16,6 +16,8 @@ export default async function GeneralSettingsPage() { preferredShell: settings.preferred_shell ?? null, shellOptions: response.shell_options ?? [], defaultEditorId: settings.default_editor_id ?? null, + enablePreviewOnClick: settings.enable_preview_on_click ?? false, + chatSubmitKey: settings.chat_submit_key ?? 'cmd_enter', loaded: true, } : undefined, diff --git a/apps/web/components/settings/editors-settings.tsx b/apps/web/components/settings/editors-settings.tsx index ebef23320c..0d99549a39 100644 --- a/apps/web/components/settings/editors-settings.tsx +++ b/apps/web/components/settings/editors-settings.tsx @@ -370,6 +370,7 @@ export function EditorsSettings() { shellOptions: currentUserSettings.shellOptions ?? [], defaultEditorId: response.settings.default_editor_id || null, enablePreviewOnClick: response.settings.enable_preview_on_click ?? false, + chatSubmitKey: response.settings.chat_submit_key ?? 'cmd_enter', loaded: true, }); } diff --git a/apps/web/components/settings/general-settings.tsx b/apps/web/components/settings/general-settings.tsx index 7205d45b99..13f55c0f2a 100644 --- a/apps/web/components/settings/general-settings.tsx +++ b/apps/web/components/settings/general-settings.tsx @@ -2,7 +2,7 @@ import { useState } from 'react'; import { useTheme } from 'next-themes'; -import { IconPalette, IconServer } from '@tabler/icons-react'; +import { IconPalette, IconServer, IconKeyboard } from '@tabler/icons-react'; import { Card, CardContent, CardHeader, CardTitle } from '@kandev/ui/card'; import { Label } from '@kandev/ui/label'; import { Input } from '@kandev/ui/input'; @@ -11,6 +11,8 @@ import { Separator } from '@kandev/ui/separator'; import { SettingsSection } from '@/components/settings/settings-section'; import { ShellSettingsCard } from '@/components/settings/shell-settings-card'; import { getBackendConfig } from '@/lib/config'; +import { useAppStore } from '@/components/state-provider'; +import { updateUserSettings } from '@/lib/api'; import type { Theme } from '@/lib/settings/types'; export function GeneralSettings() { @@ -19,6 +21,39 @@ export function GeneralSettings() { const displayBackendUrl = backendUrl.replace(/^https?:\/\//, '').replace(/\/$/, ''); const themeValue = currentTheme ?? 'system'; + // User settings + const userSettings = useAppStore((state) => state.userSettings); + const setUserSettings = useAppStore((state) => state.setUserSettings); + const [isSavingSubmitKey, setIsSavingSubmitKey] = useState(false); + + const handleChatSubmitKeyChange = async (value: 'enter' | 'cmd_enter') => { + if (isSavingSubmitKey) return; + + setIsSavingSubmitKey(true); + const previousValue = userSettings.chatSubmitKey; + + try { + setUserSettings({ + ...userSettings, + chatSubmitKey: value, + }); + + await updateUserSettings({ + workspace_id: userSettings.workspaceId || '', + board_id: userSettings.boardId || '', + repository_ids: userSettings.repositoryIds || [], + chat_submit_key: value, + }); + } catch { + setUserSettings({ + ...userSettings, + chatSubmitKey: previousValue, + }); + } finally { + setIsSavingSubmitKey(false); + } + }; + return (
@@ -62,6 +97,43 @@ export function GeneralSettings() { + } + title="Chat Input" + description="Configure chat input behavior" + > + + + Submit Shortcut + + +
+ + +

+ {userSettings.chatSubmitKey === 'cmd_enter' + ? 'Press Cmd/Ctrl+Enter to send messages. Press Enter for newlines.' + : 'Press Enter to send messages. Press Shift+Enter for newlines.'} +

+
+
+
+
+ + + } title="Backend Connection" diff --git a/apps/web/components/task/chat/chat-input-container.tsx b/apps/web/components/task/chat/chat-input-container.tsx index 8bf7871cf2..a7730d62eb 100644 --- a/apps/web/components/task/chat/chat-input-container.tsx +++ b/apps/web/components/task/chat/chat-input-container.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useRef, useCallback, useState, useEffect, memo, type KeyboardEvent as ReactKeyboardEvent } from 'react'; +import { useRef, useCallback, useState, useEffect, memo, forwardRef, useImperativeHandle, type KeyboardEvent as ReactKeyboardEvent } from 'react'; import { IconArrowUp, IconBrain, @@ -27,6 +27,7 @@ import { MentionMenu } from './mention-menu'; import { SlashCommandMenu } from './slash-command-menu'; import { ClarificationInputOverlay } from './clarification-input-overlay'; import { CommentBlocksContainer } from './comment-block'; +import { ChatInputFocusHint } from './chat-input-focus-hint'; import { useInlineMention } from '@/hooks/use-inline-mention'; import { useInlineSlash } from '@/hooks/use-inline-slash'; import type { Message } from '@/lib/types/http'; @@ -45,6 +46,7 @@ type ChatInputToolbarProps = { isSending: boolean; onCancel: () => void; onSubmit: () => void; + submitKey?: 'enter' | 'cmd_enter'; }; const ChatInputToolbar = memo(function ChatInputToolbar({ @@ -59,7 +61,10 @@ const ChatInputToolbar = memo(function ChatInputToolbar({ isSending, onCancel, onSubmit, + submitKey = 'cmd_enter', }: ChatInputToolbarProps) { + const submitShortcut = submitKey === 'enter' ? SHORTCUTS.SUBMIT_ENTER : SHORTCUTS.SUBMIT; + return (
{/* Left: Plan, Thinking, Context */} @@ -129,7 +134,7 @@ const ChatInputToolbar = memo(function ChatInputToolbar({ {/* Submit/Stop button */}
- + {isAgentBusy ? (
); -} +}); diff --git a/apps/web/components/task/chat/chat-input-focus-hint.tsx b/apps/web/components/task/chat/chat-input-focus-hint.tsx new file mode 100644 index 0000000000..52045014fd --- /dev/null +++ b/apps/web/components/task/chat/chat-input-focus-hint.tsx @@ -0,0 +1,31 @@ +'use client'; + +import { cn } from '@/lib/utils'; + +type ChatInputFocusHintProps = { + visible: boolean; + className?: string; +}; + +export function ChatInputFocusHint({ visible, className }: ChatInputFocusHintProps) { + if (!visible) return null; + + return ( +
+ + / + + to focus +
+ ); +} diff --git a/apps/web/components/task/chat/rich-text-input.tsx b/apps/web/components/task/chat/rich-text-input.tsx index fe98a91893..78f28f67f4 100644 --- a/apps/web/components/task/chat/rich-text-input.tsx +++ b/apps/web/components/task/chat/rich-text-input.tsx @@ -21,6 +21,7 @@ export type RichTextInputHandle = { getValue: () => string; setValue: (value: string) => void; insertText: (text: string, from: number, to: number) => void; + getTextareaElement: () => HTMLTextAreaElement | null; }; type RichTextInputProps = { @@ -32,6 +33,9 @@ type RichTextInputProps = { disabled?: boolean; className?: string; planModeEnabled?: boolean; + submitKey?: 'enter' | 'cmd_enter'; + onFocus?: () => void; + onBlur?: () => void; }; export const RichTextInput = forwardRef( @@ -45,6 +49,9 @@ export const RichTextInput = forwardRef disabled = false, className, planModeEnabled = false, + submitKey = 'cmd_enter', + onFocus, + onBlur, }, ref ) { @@ -164,6 +171,7 @@ export const RichTextInput = forwardRef textareaRef.current?.setSelectionRange(from + text.length, from + text.length); }); }, + getTextareaElement: () => textareaRef.current, }), [getCaretRect, onChange, value] ); @@ -179,11 +187,21 @@ export const RichTextInput = forwardRef if (event.defaultPrevented) return; } - // Submit only on Cmd/Ctrl+Enter, and only if not disabled - if (event.key === 'Enter' && (event.metaKey || event.ctrlKey)) { - event.preventDefault(); - if (!disabled) { - onSubmit?.(); + if (submitKey === 'enter') { + // Submit on Enter (unless Shift for newline) + if (event.key === 'Enter' && !event.shiftKey && !event.metaKey && !event.ctrlKey) { + event.preventDefault(); + if (!disabled) { + onSubmit?.(); + } + } + } else { + // Submit on Cmd/Ctrl+Enter (current behavior) + if (event.key === 'Enter' && (event.metaKey || event.ctrlKey)) { + event.preventDefault(); + if (!disabled) { + onSubmit?.(); + } } } }; @@ -194,6 +212,8 @@ export const RichTextInput = forwardRef value={value} onChange={handleChange} onKeyDown={handleKeyDown} + onFocus={onFocus} + onBlur={onBlur} placeholder={placeholder} disabled={disabled} className={cn( diff --git a/apps/web/components/task/task-chat-panel.tsx b/apps/web/components/task/task-chat-panel.tsx index cc8f017a20..3a69be583f 100644 --- a/apps/web/components/task/task-chat-panel.tsx +++ b/apps/web/components/task/task-chat-panel.tsx @@ -4,6 +4,8 @@ import { useCallback, useEffect, useRef, useState, memo } from 'react'; import { getWebSocketClient } from '@/lib/ws/connection'; import { useAppStore } from '@/components/state-provider'; import { getLocalStorage } from '@/lib/local-storage'; +import { useKeyboardShortcut } from '@/hooks/use-keyboard-shortcut'; +import { SHORTCUTS } from '@/lib/keyboard/constants'; import { useSessionMessages } from '@/hooks/domains/session/use-session-messages'; import { useSettingsData } from '@/hooks/domains/settings/use-settings-data'; import { useSessionState } from '@/hooks/domains/session/use-session-state'; @@ -12,7 +14,7 @@ import { useSessionModel } from '@/hooks/domains/session/use-session-model'; import { useMessageHandler } from '@/hooks/use-message-handler'; import { TodoSummary } from '@/components/task/chat/todo-summary'; import { VirtualizedMessageList } from '@/components/task/chat/virtualized-message-list'; -import { ChatInputContainer } from '@/components/task/chat/chat-input-container'; +import { ChatInputContainer, type ChatInputContainerHandle } from '@/components/task/chat/chat-input-container'; import { formatReviewCommentsAsMarkdown } from '@/components/task/chat/messages/review-comments-attachment'; import { useDiffCommentsStore, @@ -40,6 +42,7 @@ export const TaskChatPanel = memo(function TaskChatPanel({ }: TaskChatPanelProps) { const [isSending, setIsSending] = useState(false); const lastAgentMessageCountRef = useRef(0); + const chatInputRef = useRef(null); // Ensure agent profile data is loaded (may not be hydrated from SSR in all navigation paths) useSettingsData(true); @@ -104,6 +107,13 @@ export const TaskChatPanel = memo(function TaskChatPanel({ session?.agent_profile_id ); + // User settings + const chatSubmitKey = useAppStore((state) => state.userSettings.chatSubmitKey); + const agentCommands = useAppStore((state) => + resolvedSessionId ? state.availableCommands.bySessionId[resolvedSessionId] : undefined + ); + const hasAgentCommands = agentCommands && agentCommands.length > 0; + // Message sending const { handleSendMessage } = useMessageHandler( resolvedSessionId, @@ -183,6 +193,42 @@ export const TaskChatPanel = memo(function TaskChatPanel({ } }, [isSending, onSend, handleSendMessage, markCommentsSent]); + // Focus input with / shortcut (only when input is NOT focused) + useKeyboardShortcut( + SHORTCUTS.FOCUS_INPUT, + useCallback((event: KeyboardEvent) => { + // Only handle if we're not already in an input/textarea + const activeElement = document.activeElement; + const isTyping = + activeElement instanceof HTMLInputElement || + activeElement instanceof HTMLTextAreaElement; + + if (isTyping) { + // Don't intercept - let the "/" be typed normally for slash commands + return; + } + + // Not typing anywhere, so focus the chat input + const inputHandle = chatInputRef.current; + if (inputHandle) { + event.preventDefault(); // Prevent the "/" from being typed when we focus + inputHandle.focusInput(); + } + }, []), + { enabled: true, preventDefault: false } + ); + + // Cancel agent turn with ESC shortcut + useKeyboardShortcut( + SHORTCUTS.CANCEL, + useCallback(() => { + if (isAgentBusy) { + handleCancelTurn(); + } + }, [isAgentBusy, handleCancelTurn]), + { enabled: isAgentBusy } + ); + return (
@@ -207,6 +253,7 @@ export const TaskChatPanel = memo(function TaskChatPanel({
{todoItems.length > 0 && } onOpenFileAtLine(comment.filePath) : undefined} + submitKey={chatSubmitKey} + hasAgentCommands={hasAgentCommands} />
diff --git a/apps/web/hooks/domains/settings/use-editors.ts b/apps/web/hooks/domains/settings/use-editors.ts index dd3881385a..978b0cd362 100644 --- a/apps/web/hooks/domains/settings/use-editors.ts +++ b/apps/web/hooks/domains/settings/use-editors.ts @@ -52,6 +52,7 @@ export function useEditors() { shellOptions: data.shell_options ?? [], defaultEditorId: data.settings.default_editor_id || null, enablePreviewOnClick: data.settings.enable_preview_on_click ?? false, + chatSubmitKey: data.settings.chat_submit_key ?? 'cmd_enter', loaded: true, }); }) diff --git a/apps/web/hooks/use-user-display-settings.ts b/apps/web/hooks/use-user-display-settings.ts index c9c836d76c..3ca3db3d50 100644 --- a/apps/web/hooks/use-user-display-settings.ts +++ b/apps/web/hooks/use-user-display-settings.ts @@ -14,6 +14,7 @@ type DisplaySettings = { shellOptions: Array<{ value: string; label: string }>; defaultEditorId: string | null; enablePreviewOnClick: boolean; + chatSubmitKey: 'enter' | 'cmd_enter'; loaded: boolean; }; @@ -56,6 +57,7 @@ export function useUserDisplaySettings({ shellOptions: userSettings.shellOptions ?? [], defaultEditorId: userSettings.defaultEditorId ?? null, enablePreviewOnClick, + chatSubmitKey: userSettings.chatSubmitKey ?? 'cmd_enter', loaded: true, }; const sameWorkspace = normalized.workspaceId === userSettings.workspaceId; @@ -105,6 +107,7 @@ export function useUserDisplaySettings({ shellOptions: data.shell_options ?? [], defaultEditorId: data.settings.default_editor_id || null, enablePreviewOnClick: data.settings.enable_preview_on_click ?? false, + chatSubmitKey: data.settings.chat_submit_key ?? 'cmd_enter', loaded: true, }); }) diff --git a/apps/web/lib/api/domains/settings-api.ts b/apps/web/lib/api/domains/settings-api.ts index 4fa20f2fe2..cd323c7e8d 100644 --- a/apps/web/lib/api/domains/settings-api.ts +++ b/apps/web/lib/api/domains/settings-api.ts @@ -23,12 +23,13 @@ export async function fetchUserSettings(options?: ApiRequestOptions) { export async function updateUserSettings( payload: { - workspace_id: string; - board_id: string; - repository_ids: string[]; + workspace_id?: string; + board_id?: string; + repository_ids?: string[]; preferred_shell?: string; default_editor_id?: string; enable_preview_on_click?: boolean; + chat_submit_key?: 'enter' | 'cmd_enter'; }, options?: ApiRequestOptions ) { diff --git a/apps/web/lib/keyboard/constants.ts b/apps/web/lib/keyboard/constants.ts index 4ce96d9b4f..a3d219c417 100644 --- a/apps/web/lib/keyboard/constants.ts +++ b/apps/web/lib/keyboard/constants.ts @@ -60,6 +60,8 @@ export const KEYS = { SEVEN: '7', EIGHT: '8', NINE: '9', + // Symbols + SLASH: '/', } as const; // Platform types @@ -94,6 +96,9 @@ export const SHORTCUTS = { key: KEYS.ENTER, modifiers: { ctrlOrCmd: true }, }, + SUBMIT_ENTER: { + key: KEYS.ENTER, + }, SAVE: { key: KEYS.S, modifiers: { ctrlOrCmd: true }, @@ -113,5 +118,8 @@ export const SHORTCUTS = { key: KEYS.B, modifiers: { ctrlOrCmd: true }, }, + FOCUS_INPUT: { + key: KEYS.SLASH, + }, } as const; diff --git a/apps/web/lib/state/slices/settings/settings-slice.ts b/apps/web/lib/state/slices/settings/settings-slice.ts index 147d36c609..27581320f3 100644 --- a/apps/web/lib/state/slices/settings/settings-slice.ts +++ b/apps/web/lib/state/slices/settings/settings-slice.ts @@ -26,6 +26,7 @@ export const defaultSettingsState: SettingsSliceState = { shellOptions: [], defaultEditorId: null, enablePreviewOnClick: false, + chatSubmitKey: 'cmd_enter', loaded: false, }, }; diff --git a/apps/web/lib/state/slices/settings/types.ts b/apps/web/lib/state/slices/settings/types.ts index bab7f1410c..51a5bff445 100644 --- a/apps/web/lib/state/slices/settings/types.ts +++ b/apps/web/lib/state/slices/settings/types.ts @@ -76,6 +76,7 @@ export type UserSettingsState = { shellOptions: Array<{ value: string; label: string }>; defaultEditorId: string | null; enablePreviewOnClick: boolean; + chatSubmitKey: 'enter' | 'cmd_enter'; loaded: boolean; }; diff --git a/apps/web/lib/types/backend.ts b/apps/web/lib/types/backend.ts index 8d7486caf9..ef6e8e49d7 100644 --- a/apps/web/lib/types/backend.ts +++ b/apps/web/lib/types/backend.ts @@ -282,6 +282,7 @@ export type UserSettingsUpdatedPayload = { preferred_shell?: string; default_editor_id?: string; enable_preview_on_click?: boolean; + chat_submit_key?: string; updated_at?: string; }; diff --git a/apps/web/lib/types/http.ts b/apps/web/lib/types/http.ts index 69bd374bed..465326191a 100644 --- a/apps/web/lib/types/http.ts +++ b/apps/web/lib/types/http.ts @@ -316,6 +316,7 @@ export type UserSettings = { preferred_shell?: string; default_editor_id?: string; enable_preview_on_click?: boolean; + chat_submit_key?: 'enter' | 'cmd_enter'; updated_at: string; }; diff --git a/apps/web/lib/ws/handlers/users.ts b/apps/web/lib/ws/handlers/users.ts index 579425f727..2cfb6e62cd 100644 --- a/apps/web/lib/ws/handlers/users.ts +++ b/apps/web/lib/ws/handlers/users.ts @@ -16,6 +16,7 @@ export function registerUsersHandlers(store: StoreApi): WsHandlers { shellOptions: state.userSettings.shellOptions ?? [], defaultEditorId: message.payload.default_editor_id || null, enablePreviewOnClick: message.payload.enable_preview_on_click ?? false, + chatSubmitKey: (message.payload.chat_submit_key as 'enter' | 'cmd_enter') ?? 'cmd_enter', loaded: true, }, }));