From 82c71168c7c7f540bb80ccc34d3ba2d8f736a92e Mon Sep 17 00:00:00 2001 From: Michal Kopanski Date: Fri, 20 Mar 2026 21:29:49 -0400 Subject: [PATCH 1/5] fix(desktop): skip pane-nav shortcuts when input/textarea is focused Cmd+Shift+Left/Right should allow text selection in chat prompts, not hijack focus to the adjacent pane. --- .../_dashboard/workspace/$workspaceId/page.tsx | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/workspace/$workspaceId/page.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/workspace/$workspaceId/page.tsx index ed7656ab114..8493cb0cedc 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/workspace/$workspaceId/page.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/workspace/$workspaceId/page.tsx @@ -334,7 +334,14 @@ function WorkspacePage() { useAppHotkey( "PREV_PANE", - () => { + (event) => { + const target = event.target as Element; + if ( + target?.tagName === "TEXTAREA" || + target?.tagName === "INPUT" || + (target as HTMLElement)?.isContentEditable + ) + return; if (!activeTabId || !activeTab?.layout || !focusedPaneId) return; const prevPaneId = getPreviousPaneId(activeTab.layout, focusedPaneId); if (prevPaneId) { @@ -347,7 +354,14 @@ function WorkspacePage() { useAppHotkey( "NEXT_PANE", - () => { + (event) => { + const target = event.target as Element; + if ( + target?.tagName === "TEXTAREA" || + target?.tagName === "INPUT" || + (target as HTMLElement)?.isContentEditable + ) + return; if (!activeTabId || !activeTab?.layout || !focusedPaneId) return; const nextPaneId = getNextPaneId(activeTab.layout, focusedPaneId); if (nextPaneId) { From 26c7ecbcb1a4bde865f1293bbca90ca7f30daaa3 Mon Sep 17 00:00:00 2001 From: Michal Kopanski Date: Wed, 25 Mar 2026 20:35:54 -0400 Subject: [PATCH 2/5] fix(desktop): stop modifier+arrow propagation in chat textarea MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix pane-nav hotkey hijacking at the source — the prompt input now calls stopPropagation for Cmd/Ctrl+Arrow keys so they perform native text selection rather than switching panes. --- .../_dashboard/workspace/$workspaceId/page.tsx | 18 ++---------------- .../components/ai-elements/prompt-input.tsx | 8 ++++++++ 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/workspace/$workspaceId/page.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/workspace/$workspaceId/page.tsx index 8493cb0cedc..ed7656ab114 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/workspace/$workspaceId/page.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/workspace/$workspaceId/page.tsx @@ -334,14 +334,7 @@ function WorkspacePage() { useAppHotkey( "PREV_PANE", - (event) => { - const target = event.target as Element; - if ( - target?.tagName === "TEXTAREA" || - target?.tagName === "INPUT" || - (target as HTMLElement)?.isContentEditable - ) - return; + () => { if (!activeTabId || !activeTab?.layout || !focusedPaneId) return; const prevPaneId = getPreviousPaneId(activeTab.layout, focusedPaneId); if (prevPaneId) { @@ -354,14 +347,7 @@ function WorkspacePage() { useAppHotkey( "NEXT_PANE", - (event) => { - const target = event.target as Element; - if ( - target?.tagName === "TEXTAREA" || - target?.tagName === "INPUT" || - (target as HTMLElement)?.isContentEditable - ) - return; + () => { if (!activeTabId || !activeTab?.layout || !focusedPaneId) return; const nextPaneId = getNextPaneId(activeTab.layout, focusedPaneId); if (nextPaneId) { diff --git a/packages/ui/src/components/ai-elements/prompt-input.tsx b/packages/ui/src/components/ai-elements/prompt-input.tsx index ec2ddc0ca84..fc82edf9f67 100644 --- a/packages/ui/src/components/ai-elements/prompt-input.tsx +++ b/packages/ui/src/components/ai-elements/prompt-input.tsx @@ -960,6 +960,14 @@ export const PromptInputTextarea = ({ }, [controller]); const handleKeyDown: KeyboardEventHandler = (e) => { + // Prevent modifier+arrow combos from bubbling to pane-navigation hotkeys + if ( + (e.key === "ArrowLeft" || e.key === "ArrowRight") && + (e.metaKey || e.ctrlKey) + ) { + e.stopPropagation(); + } + if (e.key === "Enter") { if (isComposing || e.nativeEvent.isComposing) { return; From 1cabd89707182330de935d0b6e915a749b684717 Mon Sep 17 00:00:00 2001 From: Michal Kopanski Date: Wed, 25 Mar 2026 21:54:43 -0400 Subject: [PATCH 3/5] fix(desktop): focus chat textarea when pane receives focus via keyboard nav --- .../components/ChatInputFooter/ChatInputFooter.tsx | 10 +++++++++- .../components/ChatInputFooter/ChatInputFooter.tsx | 10 +++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/apps/desktop/src/renderer/components/Chat/ChatInterface/components/ChatInputFooter/ChatInputFooter.tsx b/apps/desktop/src/renderer/components/Chat/ChatInterface/components/ChatInputFooter/ChatInputFooter.tsx index dce68a44cfc..88c981f68f6 100644 --- a/apps/desktop/src/renderer/components/Chat/ChatInterface/components/ChatInputFooter/ChatInputFooter.tsx +++ b/apps/desktop/src/renderer/components/Chat/ChatInterface/components/ChatInputFooter/ChatInputFooter.tsx @@ -4,12 +4,13 @@ import { PromptInputAttachments, type PromptInputMessage, PromptInputTextarea, + usePromptInputController, } from "@superset/ui/ai-elements/prompt-input"; import type { ThinkingLevel } from "@superset/ui/ai-elements/thinking-toggle"; import type { ChatStatus, FileUIPart } from "ai"; import type React from "react"; import type { ReactNode } from "react"; -import { useCallback, useRef, useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import { useHotkeyText } from "renderer/stores/hotkeys"; import type { SlashCommand } from "../../hooks/useSlashCommands"; import type { ModelOption, PermissionMode } from "../../types"; @@ -74,11 +75,18 @@ export function ChatInputFooter({ onStop, onSlashCommandSend, }: ChatInputFooterProps) { + const { textInput } = usePromptInputController(); const [issueLinkOpen, setIssueLinkOpen] = useState(false); const [linkedIssues, setLinkedIssues] = useState([]); const inputRootRef = useRef(null); const errorMessage = getErrorMessage(error); const focusShortcutText = useHotkeyText("FOCUS_CHAT_INPUT"); + + useEffect(() => { + if (isFocused) { + textInput.focus(); + } + }, [isFocused, textInput]); const showFocusHint = focusShortcutText !== "Unassigned"; const addLinkedIssue = useCallback( diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceChat/components/WorkspaceChatInterface/components/ChatInputFooter/ChatInputFooter.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceChat/components/WorkspaceChatInterface/components/ChatInputFooter/ChatInputFooter.tsx index e6b00f916cf..ad48f9d8826 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceChat/components/WorkspaceChatInterface/components/ChatInputFooter/ChatInputFooter.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceChat/components/WorkspaceChatInterface/components/ChatInputFooter/ChatInputFooter.tsx @@ -4,12 +4,13 @@ import { PromptInputAttachments, type PromptInputMessage, PromptInputTextarea, + usePromptInputController, } from "@superset/ui/ai-elements/prompt-input"; import type { ThinkingLevel } from "@superset/ui/ai-elements/thinking-toggle"; import type { ChatStatus, FileUIPart } from "ai"; import type React from "react"; import type { ReactNode } from "react"; -import { useCallback, useRef, useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import { IssueLinkCommand } from "renderer/components/Chat/ChatInterface/components/IssueLinkCommand"; import { SlashCommandInput } from "renderer/components/Chat/ChatInterface/components/SlashCommandInput"; import type { SlashCommand } from "renderer/components/Chat/ChatInterface/hooks/useSlashCommands"; @@ -81,6 +82,7 @@ export function ChatInputFooter({ onStop, onSlashCommandSend, }: ChatInputFooterProps) { + const { textInput } = usePromptInputController(); const [issueLinkOpen, setIssueLinkOpen] = useState(false); const [linkedIssues, setLinkedIssues] = useState([]); const inputRootRef = useRef(null); @@ -88,6 +90,12 @@ export function ChatInputFooter({ const focusShortcutText = useHotkeyText("FOCUS_CHAT_INPUT"); const showFocusHint = focusShortcutText !== "Unassigned"; + useEffect(() => { + if (isFocused) { + textInput.focus(); + } + }, [isFocused, textInput]); + const addLinkedIssue = useCallback( (slug: string, title: string, taskId: string | undefined, url?: string) => { setLinkedIssues((prev) => { From 1259b9b2bc3343ba58d8574531e12aa9bad83fb9 Mon Sep 17 00:00:00 2001 From: Michal Kopanski Date: Wed, 25 Mar 2026 21:56:09 -0400 Subject: [PATCH 4/5] fix(desktop): place cursor at end of input when chat pane receives focus --- packages/ui/src/components/ai-elements/prompt-input.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/ui/src/components/ai-elements/prompt-input.tsx b/packages/ui/src/components/ai-elements/prompt-input.tsx index fc82edf9f67..b4eb0bf2909 100644 --- a/packages/ui/src/components/ai-elements/prompt-input.tsx +++ b/packages/ui/src/components/ai-elements/prompt-input.tsx @@ -155,7 +155,11 @@ export function PromptInputProvider({ const clearInput = useCallback(() => setTextInput(""), []); const textareaRef = useRef(null); const focus = useCallback(() => { - textareaRef.current?.focus(); + const el = textareaRef.current; + if (!el) return; + el.focus(); + const len = el.value.length; + el.setSelectionRange(len, len); }, []); const __registerTextarea = useCallback( (ref: RefObject) => { From 4a5223afeb20e33be2bd4c3e731f89a313217a40 Mon Sep 17 00:00:00 2001 From: Michal Kopanski Date: Thu, 26 Mar 2026 07:30:21 -0400 Subject: [PATCH 5/5] refactor(desktop): extract useFocusPromptOnPane shared hook --- .../components/ChatInputFooter/ChatInputFooter.tsx | 12 +++--------- .../Chat/ChatInterface/hooks/useFocusPromptOnPane.ts | 12 ++++++++++++ .../components/ChatInputFooter/ChatInputFooter.tsx | 12 +++--------- 3 files changed, 18 insertions(+), 18 deletions(-) create mode 100644 apps/desktop/src/renderer/components/Chat/ChatInterface/hooks/useFocusPromptOnPane.ts diff --git a/apps/desktop/src/renderer/components/Chat/ChatInterface/components/ChatInputFooter/ChatInputFooter.tsx b/apps/desktop/src/renderer/components/Chat/ChatInterface/components/ChatInputFooter/ChatInputFooter.tsx index 88c981f68f6..a549034d10c 100644 --- a/apps/desktop/src/renderer/components/Chat/ChatInterface/components/ChatInputFooter/ChatInputFooter.tsx +++ b/apps/desktop/src/renderer/components/Chat/ChatInterface/components/ChatInputFooter/ChatInputFooter.tsx @@ -4,13 +4,13 @@ import { PromptInputAttachments, type PromptInputMessage, PromptInputTextarea, - usePromptInputController, } from "@superset/ui/ai-elements/prompt-input"; import type { ThinkingLevel } from "@superset/ui/ai-elements/thinking-toggle"; import type { ChatStatus, FileUIPart } from "ai"; import type React from "react"; import type { ReactNode } from "react"; -import { useCallback, useEffect, useRef, useState } from "react"; +import { useCallback, useRef, useState } from "react"; +import { useFocusPromptOnPane } from "renderer/components/Chat/ChatInterface/hooks/useFocusPromptOnPane"; import { useHotkeyText } from "renderer/stores/hotkeys"; import type { SlashCommand } from "../../hooks/useSlashCommands"; import type { ModelOption, PermissionMode } from "../../types"; @@ -75,18 +75,12 @@ export function ChatInputFooter({ onStop, onSlashCommandSend, }: ChatInputFooterProps) { - const { textInput } = usePromptInputController(); + useFocusPromptOnPane(isFocused); const [issueLinkOpen, setIssueLinkOpen] = useState(false); const [linkedIssues, setLinkedIssues] = useState([]); const inputRootRef = useRef(null); const errorMessage = getErrorMessage(error); const focusShortcutText = useHotkeyText("FOCUS_CHAT_INPUT"); - - useEffect(() => { - if (isFocused) { - textInput.focus(); - } - }, [isFocused, textInput]); const showFocusHint = focusShortcutText !== "Unassigned"; const addLinkedIssue = useCallback( diff --git a/apps/desktop/src/renderer/components/Chat/ChatInterface/hooks/useFocusPromptOnPane.ts b/apps/desktop/src/renderer/components/Chat/ChatInterface/hooks/useFocusPromptOnPane.ts new file mode 100644 index 00000000000..df25e294736 --- /dev/null +++ b/apps/desktop/src/renderer/components/Chat/ChatInterface/hooks/useFocusPromptOnPane.ts @@ -0,0 +1,12 @@ +import { usePromptInputController } from "@superset/ui/ai-elements/prompt-input"; +import { useEffect } from "react"; + +export function useFocusPromptOnPane(isFocused: boolean) { + const { textInput } = usePromptInputController(); + + useEffect(() => { + if (isFocused) { + textInput.focus(); + } + }, [isFocused, textInput]); +} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceChat/components/WorkspaceChatInterface/components/ChatInputFooter/ChatInputFooter.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceChat/components/WorkspaceChatInterface/components/ChatInputFooter/ChatInputFooter.tsx index ad48f9d8826..601fa4fe76a 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceChat/components/WorkspaceChatInterface/components/ChatInputFooter/ChatInputFooter.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceChat/components/WorkspaceChatInterface/components/ChatInputFooter/ChatInputFooter.tsx @@ -4,15 +4,15 @@ import { PromptInputAttachments, type PromptInputMessage, PromptInputTextarea, - usePromptInputController, } from "@superset/ui/ai-elements/prompt-input"; import type { ThinkingLevel } from "@superset/ui/ai-elements/thinking-toggle"; import type { ChatStatus, FileUIPart } from "ai"; import type React from "react"; import type { ReactNode } from "react"; -import { useCallback, useEffect, useRef, useState } from "react"; +import { useCallback, useRef, useState } from "react"; import { IssueLinkCommand } from "renderer/components/Chat/ChatInterface/components/IssueLinkCommand"; import { SlashCommandInput } from "renderer/components/Chat/ChatInterface/components/SlashCommandInput"; +import { useFocusPromptOnPane } from "renderer/components/Chat/ChatInterface/hooks/useFocusPromptOnPane"; import type { SlashCommand } from "renderer/components/Chat/ChatInterface/hooks/useSlashCommands"; import type { ModelOption, @@ -82,7 +82,7 @@ export function ChatInputFooter({ onStop, onSlashCommandSend, }: ChatInputFooterProps) { - const { textInput } = usePromptInputController(); + useFocusPromptOnPane(isFocused); const [issueLinkOpen, setIssueLinkOpen] = useState(false); const [linkedIssues, setLinkedIssues] = useState([]); const inputRootRef = useRef(null); @@ -90,12 +90,6 @@ export function ChatInputFooter({ const focusShortcutText = useHotkeyText("FOCUS_CHAT_INPUT"); const showFocusHint = focusShortcutText !== "Unassigned"; - useEffect(() => { - if (isFocused) { - textInput.focus(); - } - }, [isFocused, textInput]); - const addLinkedIssue = useCallback( (slug: string, title: string, taskId: string | undefined, url?: string) => { setLinkedIssues((prev) => {