Explorer home chat to present a Run SQL secondary action if value is detected to be a SQL query (#50560)

## 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

<img width="740" height="210" alt="image"
src="https://github.com/user-attachments/assets/d4b1fec1-f38c-426f-8108-b50ead1a61ca"
/>



<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## 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.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Joshen Lim authored and GitHub committed 2026-09-18 14:35:35 +08:00
1 parent 85f19367ec
commit 501666e504
4 files changed
+76 -17

No files matched your search

@@ -55,6 +55,7 @@ const ExplorerHomeContent = () => {
const { createChat } = useCreateChat()
const [value, setValue] = useState<string>('')
const isSqlQuery = isSqlStatement(value)
return (
<div className="flex flex-col h-full">
@@ -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
}
/>
<AssistantAgentHarnessFooter />
@@ -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)
})
@@ -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)
}
@@ -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<HTMLFormElement, FormProps>(
@@ -70,6 +83,9 @@ const AssistantChatFormComponent = forwardRef<HTMLFormElement, FormProps>(
isEditing = false,
selectedModel,
onSelectModel,
submitIcon = <ArrowUp />,
submitLabel = 'Send message',
secondaryAction,
...props
},
_ref
@@ -134,7 +150,7 @@ const AssistantChatFormComponent = forwardRef<HTMLFormElement, FormProps>(
/>
<div
className={cn(
'absolute inset-x-1.5 bottom-1.5 flex items-center pointer-events-none',
'absolute inset-x-2 bottom-2 flex items-center pointer-events-none',
showModelSelector ? 'justify-between' : 'justify-end'
)}
>
@@ -144,7 +160,28 @@ const AssistantChatFormComponent = forwardRef<HTMLFormElement, FormProps>(
</div>
)}
<div className="flex gap-3 items-center pointer-events-auto">
<div className="flex gap-x-1.5 items-center pointer-events-auto">
<AnimatePresence>
{!loading && secondaryAction && (
<motion.div
key="secondary-action"
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.8 }}
transition={{ duration: 0.15 }}
>
<Button
type="button"
variant="outline"
aria-label={secondaryAction.label}
onClick={secondaryAction.onClick}
className="h-7 rounded-full"
>
{secondaryAction.label}
</Button>
</motion.div>
)}
</AnimatePresence>
{loading ? (
onStop ? (
<ButtonTooltip
@@ -162,14 +199,14 @@ const AssistantChatFormComponent = forwardRef<HTMLFormElement, FormProps>(
<ButtonTooltip
type="submit"
variant={canSubmit ? 'primary' : 'default'}
aria-label="Send message"
icon={<ArrowUp />}
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 } }}
/>
)}
</div>