+
ERROR: {queryError.message}
- ) : queryResult ? (
-
-
-
- ) : !isExecuting ? (
- readOnlyError ? (
- readOnlyErrorPlaceholder
- ) : (
- noResultPlaceholder
+ ) : (
+ localResults && (
+
+
+
)
- ) : null}
+ )}
>
)}
diff --git a/apps/studio/lib/ai/prompts.ts b/apps/studio/lib/ai/prompts.ts
index 4b5e1d603c1..c8c4bfb27ca 100644
--- a/apps/studio/lib/ai/prompts.ts
+++ b/apps/studio/lib/ai/prompts.ts
@@ -280,41 +280,41 @@ Developer: # Role and Objective
- Be aware that tool access may be restricted depending on the user's organization settings.
- Do not try to bypass tool restrictions by executing SQL e.g. writing a query to retrieve database schema information. Instead, explain to the user you do not have permissions to use the tools you need to execute the task
-# Output Format
+## Output Format
- Always integrate findings from the tools seamlessly into your responses for better accuracy and context.
-# Searching Docs
+## Searching Docs
- Use \`search_docs\` to search the Supabase documentation for relevant information when the question is about Supabase features or complex database operations
`
export const CHAT_PROMPT = `
-Developer: # Response Style
-- Be direct and concise. Provide only essential information.
-- Use lists to present information; do not use tables for formatting.
-- Minimize use of emojis.
+# Response Style
+- Be professional, direct and concise. Provide only essential information.
+- Before context gathering or tool usage, summarise the user's request and your plan of action in a single paragraph
+- Do not repeat yourself or your plan after context gathering
# Response Format
-## Markdown
-- Follow the CommonMark specification.
-- Use a logical heading hierarchy (H1–H4), maintaining order without skipping levels.
+## Use Markdown
+- *CRITICAL*: Response must be in markdown format.
+- Always use markdown blocks **where semantically correct** (e.g., \`inline code\`, \`\`\`code fences\`\`\`, headings, lists, tables).
+- Make use of markdown headings to structure your response where appropriate. e.g. WRONG "Section heading: ..." WRITE "## Section heading"
+- Shorter responses do not need headings
- Use bold text exclusively to emphasize key information.
- Do not use tables for displaying information under any circumstances.
+- Minimize use of emojis.
# Chat Naming
- At the start of each conversation, if the chat has not yet been named, invoke \`rename_chat\` with a descriptive 2–4 word name. Examples: "User Authentication Setup", "Sales Data Analysis", "Product Table Creation".
-## Task Workflow
-- Always start the conversation with a concise checklist of sub-tasks you will perform before generating outputs or calling tools. Keep the checklist conceptual, not implementation-level.
-- No need to repeat the checklist later in the conversation
-
# SQL Execution and Display
- Be confident: assume the user is the project owner. You do not need to show code before execution.
- To actually run SQL, directly call the \`execute_sql\` tool with the \`sql\` string. The client will request user confirmation and then return results.
- If executing SQL returns an error, explain the error concisely and try again with the correct SQL.
-- The user may skip executing the query in which case you should offer alternative options or actions to take
+- The user may skip executing the query in which case you should acknowledge the skip and offer alternative options or actions to take
- If the user asks you to write a query, or if you want to show example SQL without executing, render it in a markdown code block (e.g.: \`\`\`sql). Do this only when the user asks to see the code or for illustrative examples.
- If multiple queries are needed, call \`execute_sql\` separately for each and validate results in 1–2 lines. Use separate code blocks only for non-executed examples.
- After executing queries, summarize the outcome and confirm next actions or self-correct as needed.
+- You do not need to repeat the SQL query results as the client will display them to the user as part of the execute_sql tool call.
# Edge Functions
- Be confident: assume the user is the project owner. You do not need to show code before deployment.
diff --git a/apps/studio/lib/ai/tools/mcp-tools.ts b/apps/studio/lib/ai/tools/mcp-tools.ts
index 777916f7693..eb136848744 100644
--- a/apps/studio/lib/ai/tools/mcp-tools.ts
+++ b/apps/studio/lib/ai/tools/mcp-tools.ts
@@ -3,6 +3,8 @@ import { createSupabaseMCPClient } from '../supabase-mcp'
import { filterToolsByOptInLevel, toolSetValidationSchema } from '../tool-filter'
import type { ToolSet } from 'ai'
+const UI_EXECUTED_TOOLS = ['execute_sql', 'deploy_edge_function']
+
export const getMcpTools = async ({
accessToken,
projectRef,
@@ -24,8 +26,9 @@ export const getMcpTools = async ({
// Remove UI-executed tools handled locally
const filteredMcpTools: ToolSet = { ...allowedMcpTools }
- delete (filteredMcpTools as any)['execute_sql']
- delete (filteredMcpTools as any)['deploy_edge_function']
+ UI_EXECUTED_TOOLS.forEach((toolName) => {
+ delete filteredMcpTools[toolName]
+ })
// Validate that only known tools are provided
const validation = toolSetValidationSchema.safeParse(filteredMcpTools)
diff --git a/apps/studio/lib/ai/tools/rendering-tools.ts b/apps/studio/lib/ai/tools/rendering-tools.ts
index 973f63f1b9a..96d06f631ef 100644
--- a/apps/studio/lib/ai/tools/rendering-tools.ts
+++ b/apps/studio/lib/ai/tools/rendering-tools.ts
@@ -6,8 +6,13 @@ export const getRenderingTools = () => ({
description: 'Asks the user to execute a SQL statement and return the results',
inputSchema: z.object({
sql: z.string().describe('The SQL statement to execute.'),
+ label: z.string().describe('A short 2-4 word label for the SQL statement.'),
+ isWriteQuery: z
+ .boolean()
+ .describe(
+ 'Whether the SQL statement performs a write operation of any kind instead of a read operation'
+ ),
}),
- // No execute – runs client-side in the UI
}),
deploy_edge_function: tool({
description:
@@ -16,7 +21,6 @@ export const getRenderingTools = () => ({
name: z.string().describe('The URL-friendly name/slug of the Edge Function.'),
code: z.string().describe('The TypeScript code for the Edge Function.'),
}),
- // No execute – runs client-side in the UI
}),
rename_chat: tool({
description: `Rename the current chat session when the current chat name doesn't describe the conversation topic.`,
diff --git a/apps/studio/pages/api/ai/sql/generate-v4.ts b/apps/studio/pages/api/ai/sql/generate-v4.ts
index 91540460a04..f4278684b96 100644
--- a/apps/studio/pages/api/ai/sql/generate-v4.ts
+++ b/apps/studio/pages/api/ai/sql/generate-v4.ts
@@ -72,26 +72,6 @@ async function handlePost(req: NextApiRequest, res: NextApiResponse) {
const { messages: rawMessages, projectRef, connectionString, orgSlug, chatName } = data
- // Server-side safety: limit to last 7 messages and remove `results` property to prevent accidental leakage.
- // Results property is used to cache results client-side after queries are run
- // Tool results will still be included in history sent to model
- const messages = (rawMessages || []).slice(-7).map((msg: any) => {
- if (msg && msg.role === 'assistant' && 'results' in msg) {
- const cleanedMsg = { ...msg }
- delete cleanedMsg.results
- return cleanedMsg
- }
- // [Joshen] Am also filtering out any tool calls which state is "input-streaming"
- // this happens when a user stops the assistant response while the tool is being called
- if (msg && msg.role === 'assistant' && msg.parts) {
- const cleanedParts = msg.parts.filter((part: any) => {
- return !(part.type.startsWith('tool-') && part.state === 'input-streaming')
- })
- return { ...msg, parts: cleanedParts }
- }
- return msg
- })
-
let aiOptInLevel: AiOptInLevel = 'disabled'
let isLimited = false
@@ -117,6 +97,39 @@ async function handlePost(req: NextApiRequest, res: NextApiResponse) {
}
}
+ // Server-side safety: limit to last 7 messages and remove `results` property to prevent accidental leakage.
+ // Results property is used to cache results client-side after queries are run
+ // Tool results will still be included in history sent to model
+ const messages = (rawMessages || []).slice(-7).map((msg: any) => {
+ if (msg && msg.role === 'assistant' && 'results' in msg) {
+ const cleanedMsg = { ...msg }
+ delete cleanedMsg.results
+ return cleanedMsg
+ }
+ // Filter out tool calls with UI-specific states that the AI SDK doesn't understand
+ // These states are used for UI rendering but shouldn't be sent to the model
+ if (msg && msg.role === 'assistant' && msg.parts) {
+ const cleanedParts = msg.parts
+ .filter((part: any) => {
+ // Filter out tool calls with invalid states for AI SDK
+ if (part.type.startsWith('tool-')) {
+ const invalidStates = ['input-streaming', 'input-available', 'output-error']
+ return !invalidStates.includes(part.state)
+ }
+ return true
+ })
+ .map((part: any) => {
+ // If opt in level is schema_and_log_and_data, replace part output with permission message
+ if (aiOptInLevel !== 'schema_and_log_and_data' && part.output) {
+ return { ...part, output: 'Result not shared due to permission level' }
+ }
+ return part
+ })
+ return { ...msg, parts: cleanedParts }
+ }
+ return msg
+ })
+
const {
model,
error: modelError,
diff --git a/apps/studio/state/ai-assistant-state.tsx b/apps/studio/state/ai-assistant-state.tsx
index 4d2836c070c..62e46842231 100644
--- a/apps/studio/state/ai-assistant-state.tsx
+++ b/apps/studio/state/ai-assistant-state.tsx
@@ -104,16 +104,34 @@ async function clearStorage(): Promise
{
}
}
+// Helper function to sanitize objects to ensure they're cloneable
+function sanitizeForCloning(obj: any): any {
+ if (obj === null || obj === undefined) return obj
+ if (typeof obj !== 'object') return obj
+
+ // Convert to plain object to remove any proxy wrappers
+ return JSON.parse(JSON.stringify(obj))
+}
+
// Helper function to load state from IndexedDB
async function loadFromIndexedDB(projectRef: string): Promise {
try {
const persistedState = await getAiState(projectRef)
if (persistedState) {
- // Revive dates
+ // Revive dates and sanitize message data
Object.values(persistedState.chats).forEach((chat: ChatSession) => {
if (chat && typeof chat === 'object') {
chat.createdAt = new Date(chat.createdAt)
chat.updatedAt = new Date(chat.updatedAt)
+
+ // Sanitize message parts to remove proxy objects
+ if (chat.messages) {
+ chat.messages.forEach((message: any) => {
+ if (message.parts) {
+ message.parts = message.parts.map((part: any) => sanitizeForCloning(part))
+ }
+ })
+ }
}
})
return persistedState
@@ -337,26 +355,15 @@ export const createAiAssistantState = (): AiAssistantState => {
}
},
- updateMessage: ({
- id,
- resultId,
- results,
- }: {
- id: string
- resultId?: string
- results: any[]
- }) => {
+ updateMessage: (updatedMessage: MessageType) => {
const chat = state.activeChat
- if (!chat || !resultId) return
-
- const messageIndex = chat.messages.findIndex((msg) => msg.id === id)
+ if (!chat) return
+ const messageIndex = chat.messages.findIndex((msg) => msg.id === updatedMessage.id)
if (messageIndex !== -1) {
- const msg = chat.messages[messageIndex]
- if (!msg.results) {
- msg.results = {}
- }
- msg.results[resultId] = results
+ // Replace entire message instance to capture latest tool parts/results
+ chat.messages[messageIndex] = updatedMessage as AssistantMessageType
+ chat.updatedAt = new Date()
}
},
@@ -435,7 +442,7 @@ export type AiAssistantState = AiAssistantData & {
clearMessages: () => void
deleteMessagesAfter: (id: string, options?: { includeSelf?: boolean }) => void
saveMessage: (message: MessageType | MessageType[]) => void
- updateMessage: (args: { id: string; resultId?: string; results: any[] }) => void
+ updateMessage: (message: MessageType) => void
setSqlSnippets: (snippets: SqlSnippet[]) => void
clearSqlSnippets: () => void
getCachedSQLResults: (args: { messageId: string; snippetId?: string }) => any[] | undefined