From 501666e50497302e6d8c0e80fc7a1266d445e186 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Fri, 18 Sep 2026 14:35:35 +0800 Subject: [PATCH] Explorer home chat to present a Run SQL secondary action if value is detected to be a SQL query (#50560) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Context We previously introduced a behaviour for the explorer home tab's chat form to run a SQL Query if the input is detected to be a SQL query. Adjusting it to shift that behaviour into a secondary action instead image ## Summary by CodeRabbit * **New Features** * SQL statements entered in Explorer can be run directly with a dedicated “Run SQL” action. * Assistant forms support context-specific submit icons, labels, tooltips, and accessibility text. * **Bug Fixes** * Improved SQL detection for multi-statement queries. * Prevented mixed SQL and conversational text from being treated as executable SQL. --- .../interfaces/Explorer/ExplorerHomeTab.tsx | 13 +++-- .../Explorer/ExplorerHomeTab.utils.test.ts | 2 + .../Explorer/ExplorerHomeTab.utils.ts | 27 +++++++--- .../ui/AIAssistantPanel/AssistantChatForm.tsx | 51 ++++++++++++++++--- 4 files changed, 76 insertions(+), 17 deletions(-) diff --git a/apps/studio/components/interfaces/Explorer/ExplorerHomeTab.tsx b/apps/studio/components/interfaces/Explorer/ExplorerHomeTab.tsx index 4accbab5a83..cb391fbda93 100644 --- a/apps/studio/components/interfaces/Explorer/ExplorerHomeTab.tsx +++ b/apps/studio/components/interfaces/Explorer/ExplorerHomeTab.tsx @@ -55,6 +55,7 @@ const ExplorerHomeContent = () => { const { createChat } = useCreateChat() const [value, setValue] = useState('') + const isSqlQuery = isSqlStatement(value) return (
@@ -75,10 +76,14 @@ const ExplorerHomeContent = () => { placeholder="Explore your data, check project health, create a notebook..." value={value} onValueChange={(e) => setValue(e.target.value)} - onSubmit={(message) => - isSqlStatement(message) - ? createQuery({ sql: message, autoRun: true }) - : createChat({ initialMessage: message }) + onSubmit={(message) => createChat({ initialMessage: message })} + secondaryAction={ + isSqlQuery + ? { + label: 'Run SQL', + onClick: () => createQuery({ sql: value, autoRun: true }), + } + : undefined } /> diff --git a/apps/studio/components/interfaces/Explorer/ExplorerHomeTab.utils.test.ts b/apps/studio/components/interfaces/Explorer/ExplorerHomeTab.utils.test.ts index 805bdc73e97..192839ca529 100644 --- a/apps/studio/components/interfaces/Explorer/ExplorerHomeTab.utils.test.ts +++ b/apps/studio/components/interfaces/Explorer/ExplorerHomeTab.utils.test.ts @@ -19,6 +19,7 @@ describe('isSqlStatement', () => { 'SHOW ALL;', 'set search_path to public', "SET TIME ZONE 'UTC'", + 'select * from a;\n\nselect * from b;', ])('returns true for %s', (message) => { expect(isSqlStatement(message)).toBe(true) }) @@ -34,6 +35,7 @@ describe('isSqlStatement', () => { 'Show me my tables', 'Set up RLS on my users table', 'With my current schema, what tables should I add?', + 'select * from colors;\n\nhelp me figure out what is wrong with this', ])('returns false for %s', (message) => { expect(isSqlStatement(message)).toBe(false) }) diff --git a/apps/studio/components/interfaces/Explorer/ExplorerHomeTab.utils.ts b/apps/studio/components/interfaces/Explorer/ExplorerHomeTab.utils.ts index 37358997ce4..f96c7ac2316 100644 --- a/apps/studio/components/interfaces/Explorer/ExplorerHomeTab.utils.ts +++ b/apps/studio/components/interfaces/Explorer/ExplorerHomeTab.utils.ts @@ -17,17 +17,32 @@ const SET_STATEMENT_REGEX = /^\s*set\s+(session\s+|local\s+)?(time\s+zone\b|[a-zA-Z_][a-zA-Z0-9_.]*\s*(=|to)\s*\S)/i const WITH_STATEMENT_REGEX = /^\s*with\s+(recursive\s+)?[a-zA-Z_][a-zA-Z0-9_]*\s+as\s*\(/i +const isSqlLikeSegment = (segment: string): boolean => + UNAMBIGUOUS_SQL_STATEMENT_REGEX.test(segment) || + CREATE_STATEMENT_REGEX.test(segment) || + SHOW_STATEMENT_REGEX.test(segment) || + SET_STATEMENT_REGEX.test(segment) || + WITH_STATEMENT_REGEX.test(segment) + /** * Whether `message` looks like a SQL statement rather than a natural-language chat prompt, * so the Explorer home tab can route it to a query tab instead of creating an AI chat. */ export function isSqlStatement(message: string): boolean { - const sql = removeCommentsFromSql(message) - return ( + const sql = removeCommentsFromSql(message).trim() + + const isSimpleStatement = UNAMBIGUOUS_SQL_STATEMENT_REGEX.test(sql) || - CREATE_STATEMENT_REGEX.test(sql) || SHOW_STATEMENT_REGEX.test(sql) || - SET_STATEMENT_REGEX.test(sql) || - WITH_STATEMENT_REGEX.test(sql) - ) + SET_STATEMENT_REGEX.test(sql) + const isComplexStatement = CREATE_STATEMENT_REGEX.test(sql) || WITH_STATEMENT_REGEX.test(sql) + + if (isComplexStatement) return true + if (!isSimpleStatement) return false + + // Simple statements (select/insert/update/.../show/set) are rarely legitimately + // multi-paragraph, so a trailing paragraph that doesn't itself look like SQL means + // this is a chat message that happens to include a query, not a query to run. + const [, ...trailingParagraphs] = sql.split(/\n\s*\n/) + return trailingParagraphs.every(isSqlLikeSegment) } diff --git a/apps/studio/components/ui/AIAssistantPanel/AssistantChatForm.tsx b/apps/studio/components/ui/AIAssistantPanel/AssistantChatForm.tsx index e5a6c8a4c9f..0c3a2d1600b 100644 --- a/apps/studio/components/ui/AIAssistantPanel/AssistantChatForm.tsx +++ b/apps/studio/components/ui/AIAssistantPanel/AssistantChatForm.tsx @@ -1,7 +1,8 @@ import { useBreakpoint } from 'common' +import { AnimatePresence, motion } from 'framer-motion' import { ArrowUp, Loader2, Square } from 'lucide-react' -import { ChangeEvent, FormEvent, forwardRef, KeyboardEvent, memo, useRef } from 'react' -import { cn, ExpandingTextArea } from 'ui' +import { ChangeEvent, FormEvent, forwardRef, KeyboardEvent, memo, ReactNode, useRef } from 'react' +import { Button, cn, ExpandingTextArea } from 'ui' import { ButtonTooltip } from '../ButtonTooltip' import { formatAttachedSnippets } from './AIAssistant.utils' @@ -49,6 +50,18 @@ export interface FormProps { selectedModel?: AssistantModelId /* Callback when a model is chosen */ onSelectModel?: (model: AssistantModelId) => void + /* Icon for the submit button when not loading, defaults to an up arrow */ + submitIcon?: ReactNode + /* Label for the submit button when not loading, used for both aria-label and tooltip text, defaults to "Send message" */ + submitLabel?: string + /** + * An additional action shown beside the main submit button, transitioned in/out with + * framer-motion. Pass undefined to hide it (e.g. when the input doesn't match its condition). + */ + secondaryAction?: { + label: string + onClick: () => void + } } const AssistantChatFormComponent = forwardRef( @@ -70,6 +83,9 @@ const AssistantChatFormComponent = forwardRef( isEditing = false, selectedModel, onSelectModel, + submitIcon = , + submitLabel = 'Send message', + secondaryAction, ...props }, _ref @@ -134,7 +150,7 @@ const AssistantChatFormComponent = forwardRef( />
@@ -144,7 +160,28 @@ const AssistantChatFormComponent = forwardRef(
)} -
+
+ + {!loading && secondaryAction && ( + + + + )} + {loading ? ( onStop ? ( ( } + aria-label={submitLabel} + icon={submitIcon} disabled={!canSubmit} className={cn( 'w-7 h-7 rounded-full p-0 text-center flex items-center justify-center', !canSubmit ? 'opacity-50' : 'opacity-100' )} - tooltip={{ content: { side: 'top', text: 'Send message' } }} + tooltip={{ content: { side: 'top', text: submitLabel } }} /> )}