diff --git a/apps/studio/components/interfaces/Integrations/Wrappers/WrapperRow.tsx b/apps/studio/components/interfaces/Integrations/Wrappers/WrapperRow.tsx
index b79a47aa27503..f64438c7d7d9d 100644
--- a/apps/studio/components/interfaces/Integrations/Wrappers/WrapperRow.tsx
+++ b/apps/studio/components/interfaces/Integrations/Wrappers/WrapperRow.tsx
@@ -8,7 +8,16 @@ import { useParams } from 'common'
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import type { FDW } from 'data/fdw/fdws-query'
import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions'
-import { Badge, Sheet, SheetContent, TableCell, TableRow } from 'ui'
+import {
+ Badge,
+ Sheet,
+ SheetContent,
+ TableCell,
+ TableRow,
+ Tooltip,
+ TooltipContent,
+ TooltipTrigger,
+} from 'ui'
import { INTEGRATIONS } from '../Landing/Integrations.constants'
import DeleteWrapperModal from './DeleteWrapperModal'
import { EditWrapperSheet } from './EditWrapperSheet'
@@ -74,7 +83,12 @@ const WrapperRow = ({ wrapper }: WrapperRowProps) => {
{integration.icon({ className: 'p-0' })}
- {target}{' '}
+
+ {target}
+
+ {target}
+
+
{
- {table.schema}.{table.table_name}
+
+
+ {table.schema}.{table.table_name}
+
+
+ {table.schema}.{table.table_name}
+
+
diff --git a/apps/studio/components/ui/AIAssistantPanel/AIAssistant.tsx b/apps/studio/components/ui/AIAssistantPanel/AIAssistant.tsx
index ad98e71ee94b9..b189896edd748 100644
--- a/apps/studio/components/ui/AIAssistantPanel/AIAssistant.tsx
+++ b/apps/studio/components/ui/AIAssistantPanel/AIAssistant.tsx
@@ -42,6 +42,7 @@ const MemoizedMessage = memo(
message,
isLoading,
onResults,
+ onDelete,
onEdit,
isAfterEditedMessage,
isBeingEdited,
@@ -58,6 +59,7 @@ const MemoizedMessage = memo(
resultId?: string
results: any[]
}) => void
+ onDelete: (id: string) => void
onEdit: (id: string) => void
isAfterEditedMessage: boolean
isBeingEdited: boolean
@@ -70,6 +72,7 @@ const MemoizedMessage = memo(
readOnly={message.role === 'user'}
isLoading={isLoading}
onResults={onResults}
+ onDelete={onDelete}
onEdit={onEdit}
isAfterEditedMessage={isAfterEditedMessage}
isBeingEdited={isBeingEdited}
@@ -105,7 +108,7 @@ export const AIAssistant = ({ className }: AIAssistantProps) => {
)
const inputRef = useRef(null)
- const { ref: scrollContainerRef, isSticky, scrollToEnd } = useAutoScroll()
+ const { ref: scrollContainerRef, isSticky, scrollToEnd, setIsSticky } = useAutoScroll()
const { aiOptInLevel, isHipaaProjectDisallowed } = useOrgAiOptInLevel()
const showMetadataWarning =
@@ -119,6 +122,7 @@ export const AIAssistant = ({ className }: AIAssistantProps) => {
const [value, setValue] = useState(snap.initialInput || '')
const [isConfirmOptInModalOpen, setIsConfirmOptInModalOpen] = useState(false)
const [editingMessageId, setEditingMessageId] = useState(null)
+ const [isResubmitting, setIsResubmitting] = useState(false)
const { data: check, isSuccess } = useCheckOpenAIKeyQuery()
const isApiKeySet = IS_PLATFORM || !!check?.hasKey
@@ -232,7 +236,7 @@ export const AIAssistant = ({ className }: AIAssistantProps) => {
onFinish: handleChatFinish,
})
- const isChatLoading = chatStatus === 'submitted' || chatStatus === 'streaming'
+ const isChatLoading = chatStatus === 'submitted' || chatStatus === 'streaming' || isResubmitting
const updateMessage = useCallback(
({
@@ -249,6 +253,22 @@ export const AIAssistant = ({ className }: AIAssistantProps) => {
[snap]
)
+ const deleteMessageFromHere = useCallback(
+ (messageId: string) => {
+ // Find the message index in current chatMessages
+ const messageIndex = chatMessages.findIndex((msg) => msg.id === messageId)
+ if (messageIndex === -1) return
+
+ if (isChatLoading) stop()
+
+ snap.deleteMessagesAfter(messageId, { includeSelf: true })
+
+ const updatedMessages = chatMessages.slice(0, messageIndex)
+ setMessages(updatedMessages)
+ },
+ [snap, setMessages, chatMessages, isChatLoading, stop]
+ )
+
const editMessage = useCallback(
(messageId: string) => {
const messageIndex = chatMessages.findIndex((msg) => msg.id === messageId)
@@ -266,9 +286,16 @@ export const AIAssistant = ({ className }: AIAssistantProps) => {
.join('') ?? ''
setValue(textContent)
- if (inputRef.current) {
- inputRef.current.focus()
- }
+ setTimeout(() => {
+ if (inputRef.current) {
+ inputRef?.current?.focus()
+
+ // [Joshen] This is just to make the cursor go to the end of the text when focusing
+ const val = inputRef.current.value
+ inputRef.current.value = ''
+ inputRef.current.value = val
+ }
+ }, 100)
},
[chatMessages, setValue]
)
@@ -292,6 +319,7 @@ export const AIAssistant = ({ className }: AIAssistantProps) => {
message={message}
isLoading={isChatLoading && message.id === chatMessages[chatMessages.length - 1].id}
onResults={updateMessage}
+ onDelete={deleteMessageFromHere}
onEdit={editMessage}
isAfterEditedMessage={isAfterEditedMessage}
isBeingEdited={isBeingEdited}
@@ -299,7 +327,15 @@ export const AIAssistant = ({ className }: AIAssistantProps) => {
/>
)
}),
- [chatMessages, isChatLoading, updateMessage, editMessage, editingMessageId, cancelEdit]
+ [
+ chatMessages,
+ isChatLoading,
+ updateMessage,
+ deleteMessageFromHere,
+ editMessage,
+ cancelEdit,
+ editingMessageId,
+ ]
)
const hasMessages = chatMessages.length > 0
@@ -308,12 +344,9 @@ export const AIAssistant = ({ className }: AIAssistantProps) => {
const sendMessageToAssistant = (finalContent: string) => {
if (editingMessageId) {
// Handling when the user is in edit mode
- const messageIndex = chatMessages.findIndex((msg) => msg.id === editingMessageId)
- if (messageIndex === -1) return
-
- snap.deleteMessagesAfter(editingMessageId, { includeSelf: true })
- const updatedMessages = chatMessages.slice(0, messageIndex)
- setMessages(updatedMessages)
+ // delete the message(s) from the chat just like the delete button
+ setIsResubmitting(true)
+ deleteMessageFromHere(editingMessageId)
setEditingMessageId(null)
}
@@ -355,6 +388,14 @@ export const AIAssistant = ({ className }: AIAssistantProps) => {
setEditingMessageId(null)
}
+ useEffect(() => {
+ // Keep "Thinking" visible while stopping and resubmitting during edit
+ // Only clear once the new response actually starts streaming (or errors)
+ if (isResubmitting && (chatStatus === 'streaming' || !!error)) {
+ setIsResubmitting(false)
+ }
+ }, [isResubmitting, chatStatus, error])
+
// Update scroll behavior for new messages
useEffect(() => {
if (!isChatLoading) {
@@ -381,6 +422,10 @@ export const AIAssistant = ({ className }: AIAssistantProps) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [snap.open, isInSQLEditor, snippetContent])
+ useEffect(() => {
+ if (chatMessages.length === 0 && !isSticky) setIsSticky(true)
+ }, [chatMessages, isSticky, setIsSticky])
+
return (
{
'z-20 [&>form>textarea]:text-base [&>form>textarea]:md:text-sm [&>form>textarea]:border-1 [&>form>textarea]:rounded-md [&>form>textarea]:!outline-none [&>form>textarea]:!ring-offset-0 [&>form>textarea]:!ring-0'
)}
loading={isChatLoading}
- disabled={!isApiKeySet || disablePrompts || isChatLoading}
+ isEditing={!!editingMessageId}
+ disabled={!isApiKeySet || disablePrompts || (isChatLoading && !editingMessageId)}
placeholder={
hasMessages
? 'Ask a follow up question...'
diff --git a/apps/studio/components/ui/AIAssistantPanel/AssistantChatForm.tsx b/apps/studio/components/ui/AIAssistantPanel/AssistantChatForm.tsx
index 58c954f9a091d..edf949772a2b4 100644
--- a/apps/studio/components/ui/AIAssistantPanel/AssistantChatForm.tsx
+++ b/apps/studio/components/ui/AIAssistantPanel/AssistantChatForm.tsx
@@ -41,6 +41,8 @@ export interface FormProps {
snippetsClassName?: string
/* Additional class name for the form wrapper */
className?: string
+ /* If currently editing an existing message */
+ isEditing?: boolean
}
const AssistantChatFormComponent = forwardRef(
@@ -59,6 +61,7 @@ const AssistantChatFormComponent = forwardRef(
snippetsClassName,
includeSnippetsInMessage = false,
className,
+ isEditing = false,
...props
},
ref
@@ -68,7 +71,7 @@ const AssistantChatFormComponent = forwardRef(
const handleSubmit = (event?: FormEvent) => {
if (event) event.preventDefault()
- if (!value || loading) return
+ if (!value || (loading && !isEditing)) return
let finalMessage = value
if (includeSnippetsInMessage && sqlSnippets && sqlSnippets.length > 0) {
@@ -107,8 +110,8 @@ const AssistantChatFormComponent = forwardRef(
/>
)}
void
+ onCancel: () => void
+}
+
+export const DeleteMessageConfirmModal = ({
+ visible,
+ onConfirm,
+ onCancel,
+}: DeleteMessageConfirmModalProps) => {
+ const onOpenChange = (open: boolean) => {
+ if (!open) onCancel()
+ }
+
+ return (
+
+ )
+}
diff --git a/apps/studio/components/ui/AIAssistantPanel/Message.tsx b/apps/studio/components/ui/AIAssistantPanel/Message.tsx
index 3a295e2d6ce25..c30bb9776a6a5 100644
--- a/apps/studio/components/ui/AIAssistantPanel/Message.tsx
+++ b/apps/studio/components/ui/AIAssistantPanel/Message.tsx
@@ -1,9 +1,10 @@
import { UIMessage as VercelMessage } from '@ai-sdk/react'
-import { Loader2, Pencil } from 'lucide-react'
-import { createContext, PropsWithChildren, ReactNode, useMemo } from 'react'
+import { Loader2, Pencil, Trash2 } from 'lucide-react'
+import { createContext, PropsWithChildren, ReactNode, useMemo, useState } from 'react'
import ReactMarkdown from 'react-markdown'
import { Components } from 'react-markdown/lib/ast-to-react'
import remarkGfm from 'remark-gfm'
+import { toast } from 'sonner'
import { ProfileImage } from 'components/ui/ProfileImage'
import { useProfile } from 'lib/profile'
@@ -11,6 +12,7 @@ import { cn, markdownComponents, WarningIcon } from 'ui'
import { ButtonTooltip } from '../ButtonTooltip'
import { EdgeFunctionBlock } from '../EdgeFunctionBlock/EdgeFunctionBlock'
import { DisplayBlockRenderer } from './DisplayBlockRenderer'
+import { DeleteMessageConfirmModal } from './DeleteMessageConfirmModal'
import {
Heading3,
Hyperlink,
@@ -51,6 +53,7 @@ interface MessageProps {
resultId?: string
results: any[]
}) => void
+ onDelete: (id: string) => void
onEdit: (id: string) => void
isAfterEditedMessage: boolean
isBeingEdited: boolean
@@ -65,12 +68,14 @@ export const Message = function Message({
action = null,
variant = 'default',
onResults,
+ onDelete,
onEdit,
isAfterEditedMessage = false,
isBeingEdited = false,
onCancelEdit,
}: PropsWithChildren) {
const { profile } = useProfile()
+ const [showDeleteConfirmModal, setShowDeleteConfirmModal] = useState(false)
const allMarkdownComponents: Partial = useMemo(
() => ({
...markdownComponents,
@@ -237,30 +242,54 @@ export const Message = function Message({
Assistant is thinking...
)}
- {/* Action button - only show for user messages on hover */}
-
+ {/* Action buttons - only show for user messages on hover */}
+
{message.role === 'user' && (
- }
- onClick={isBeingEdited || isAfterEditedMessage ? onCancelEdit : () => onEdit(id)}
- className="text-foreground-light hover:text-foreground p-1 rounded"
- aria-label={
- isBeingEdited || isAfterEditedMessage ? 'Cancel editing' : 'Edit message'
- }
- tooltip={{
- content: {
- side: 'bottom',
- text:
- isBeingEdited || isAfterEditedMessage ? 'Cancel editing' : 'Edit message',
- },
- }}
- />
+ <>
+ }
+ onClick={
+ isBeingEdited || isAfterEditedMessage ? onCancelEdit : () => onEdit(id)
+ }
+ className="text-foreground-light hover:text-foreground p-1 rounded"
+ aria-label={
+ isBeingEdited || isAfterEditedMessage ? 'Cancel editing' : 'Edit message'
+ }
+ tooltip={{
+ content: {
+ side: 'bottom',
+ text:
+ isBeingEdited || isAfterEditedMessage ? 'Cancel editing' : 'Edit message',
+ },
+ }}
+ />
+
+ }
+ tooltip={{ content: { side: 'bottom', text: 'Delete message' } }}
+ onClick={() => setShowDeleteConfirmModal(true)}
+ className="text-foreground-light hover:text-foreground p-1 rounded"
+ title="Delete message"
+ aria-label="Delete message"
+ />
+ >
)}
+
+ {
+ onDelete(id)
+ setShowDeleteConfirmModal(false)
+ toast.success('Message deleted successfully')
+ }}
+ onCancel={() => setShowDeleteConfirmModal(false)}
+ />
)
}
diff --git a/apps/studio/components/ui/AIAssistantPanel/hooks.ts b/apps/studio/components/ui/AIAssistantPanel/hooks.ts
index 507c0708e5572..9d5c9b231ae0f 100644
--- a/apps/studio/components/ui/AIAssistantPanel/hooks.ts
+++ b/apps/studio/components/ui/AIAssistantPanel/hooks.ts
@@ -69,5 +69,5 @@ export function useAutoScroll({ enabled = true }: UseAutoScrollProps = {}) {
}
}, [container, enabled, scrollToEnd])
- return { ref, isSticky, scrollToEnd }
+ return { ref, isSticky, scrollToEnd, setIsSticky }
}
diff --git a/apps/studio/csp.js b/apps/studio/csp.js
index 69108c1e06e09..c184c789d86d8 100644
--- a/apps/studio/csp.js
+++ b/apps/studio/csp.js
@@ -31,6 +31,8 @@ const isDevOrStaging =
process.env.NEXT_PUBLIC_ENVIRONMENT === 'local' ||
process.env.NEXT_PUBLIC_ENVIRONMENT === 'staging'
+const NIMBUS_STAGING_PROJECTS_URL = 'https://*.supabase-nimbus-projects.com'
+const NIMBUS_STAGING_PROJECTS_URL_WS = 'wss://*.supabase-nimbus-projects.com'
const SUPABASE_STAGING_PROJECTS_URL = 'https://*.supabase.red'
const SUPABASE_STAGING_PROJECTS_URL_WS = 'wss://*.supabase.red'
const SUPABASE_COM_URL = 'https://supabase.com'
@@ -126,6 +128,8 @@ module.exports.getCSP = function getCSP() {
? [
SUPABASE_STAGING_PROJECTS_URL,
SUPABASE_STAGING_PROJECTS_URL_WS,
+ NIMBUS_STAGING_PROJECTS_URL,
+ NIMBUS_STAGING_PROJECTS_URL_WS,
VERCEL_LIVE_URL,
SUPABASE_DOCS_PROJECT_URL,
SUPABASE_CONTENT_API_URL,
@@ -140,7 +144,9 @@ module.exports.getCSP = function getCSP() {
`blob:`,
`data:`,
...IMG_SRC_URLS,
- ...(isDevOrStaging ? [SUPABASE_STAGING_PROJECTS_URL, VERCEL_URL] : []),
+ ...(isDevOrStaging
+ ? [SUPABASE_STAGING_PROJECTS_URL, NIMBUS_STAGING_PROJECTS_URL, VERCEL_URL]
+ : []),
].join(' ')
const scriptSrcDirective = [
diff --git a/apps/studio/instrumentation-client.ts b/apps/studio/instrumentation-client.ts
index 1c07a5b3c93d4..e1543a0e1507f 100644
--- a/apps/studio/instrumentation-client.ts
+++ b/apps/studio/instrumentation-client.ts
@@ -152,3 +152,6 @@ function standardiseRouterUrl(url: string) {
return finalUrl
}
+
+// This export will instrument router navigations, and is only relevant if you enable tracing.
+export const onRouterTransitionStart = Sentry.captureRouterTransitionStart
diff --git a/turbo.json b/turbo.json
index 0409cabc4c9ae..cefe8e664e628 100644
--- a/turbo.json
+++ b/turbo.json
@@ -62,6 +62,7 @@
"NEXT_PUBLIC_GOTRUE_URL",
"NEXT_PUBLIC_VERCEL_BRANCH_URL",
"NEXT_PUBLIC_GOOGLE_MAPS_KEY",
+ "NEXT_RUNTIME",
"NODE_ENV",
"SUPABASE_URL",
// These envs are used in the packages