From 48640314669c5b144498345db2dfc47a68f01202 Mon Sep 17 00:00:00 2001 From: Ali Waseem Date: Mon, 20 Jul 2026 10:25:42 -0600 Subject: [PATCH 1/6] fix(assistant): SQL approval buttons do nothing (#48102) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes FE-3954: clicking Skip/Run Query in the AI Assistant did nothing. ## Root cause `onFinish` synced the AI SDK `Chat` instance's live message array directly into valtio state (`chat.messages = messages`). valtio's `proxy()` mutates an object's nested properties in place instead of cloning them, so this corrupted the SDK's own array with Proxies. The next approval click hit the SDK's internal `structuredClone()` call and threw `DOMException: Proxy object could not be cloned` — an unhandled rejection before any network request, so the buttons silently did nothing. ## Fix Assign a sanitized copy of the message array instead of the SDK's live reference. ## Test plan - [x] `pnpm --filter studio exec vitest run state/ai-assistant-state.test.ts` — fails on old code with the exact DOMException, passes on the fix - [ ] Manual: approve/skip a suggested query in AI Assistant and confirm it runs/is skipped ## Summary by CodeRabbit * **Bug Fixes** * Improved AI assistant chat message synchronization to prevent message corruption. * Ensured chat messages remain safely cloneable after approval-related updates. * **Tests** * Added coverage verifying that synchronized AI assistant messages can be cloned successfully. --- apps/studio/state/ai-assistant-state.test.ts | 46 ++++++++++++++++++++ apps/studio/state/ai-assistant-state.tsx | 5 ++- 2 files changed, 49 insertions(+), 2 deletions(-) create mode 100644 apps/studio/state/ai-assistant-state.test.ts diff --git a/apps/studio/state/ai-assistant-state.test.ts b/apps/studio/state/ai-assistant-state.test.ts new file mode 100644 index 0000000000000..4fb412ffe831f --- /dev/null +++ b/apps/studio/state/ai-assistant-state.test.ts @@ -0,0 +1,46 @@ +import { proxy, ref } from 'valtio/vanilla' +import { describe, expect, it } from 'vitest' + +import { sanitizeForCloning } from './ai-assistant-state' + +describe('AI assistant chat message sync', () => { + // FE-3954: syncing the live array into valtio corrupted it with Proxies, breaking structuredClone in addToolApprovalResponse + it('does not corrupt the live SDK message array when synced into valtio state', () => { + const state = proxy<{ chats: Record; chatInstances: Record }>({ + chats: {}, + chatInstances: {}, + }) + state.chats['chat-1'] = { id: 'chat-1', messages: [] } + + const liveSdkMessages = [ + { + id: 'message-1', + role: 'assistant', + parts: [ + { type: 'text', text: "Sure, here's a query" }, + { + type: 'tool-execute_sql', + toolCallId: 'tool-1', + state: 'approval-requested', + approval: { id: 'approval-1' }, + }, + ], + }, + ] + state.chatInstances['chat-1'] = ref({ messages: liveSdkMessages }) + + const chat = state.chats['chat-1'] + chat.messages = liveSdkMessages.map((message) => sanitizeForCloning(message)) + + // mirrors addToolApprovalResponse's own update logic + const lastMessage = liveSdkMessages[liveSdkMessages.length - 1] + const updatedParts = lastMessage.parts.map((part: any) => + part.state === 'approval-requested' && part.approval?.id === 'approval-1' + ? { ...part, state: 'approval-responded', approval: { id: 'approval-1', approved: true } } + : part + ) + const replacedMessage = { ...lastMessage, parts: updatedParts } + + expect(() => structuredClone(replacedMessage)).not.toThrow() + }) +}) diff --git a/apps/studio/state/ai-assistant-state.tsx b/apps/studio/state/ai-assistant-state.tsx index c083fa10e52c7..9f9599b32f9bc 100644 --- a/apps/studio/state/ai-assistant-state.tsx +++ b/apps/studio/state/ai-assistant-state.tsx @@ -152,7 +152,7 @@ async function clearStorage(): Promise { // Helper function to sanitize objects to ensure they're cloneable // Issue due to addToolResult -function sanitizeForCloning(obj: any): any { +export function sanitizeForCloning(obj: any): any { if (obj === null || obj === undefined) return obj if (typeof obj !== 'object') return obj return JSON.parse(JSON.stringify(obj)) @@ -329,7 +329,8 @@ function createChatInstance( const messages = chatInstance.messages const chat = state.chats[options.id] if (chat) { - chat.messages = messages + // Clone first — valtio's proxy() mutates nested properties in place and would corrupt the SDK's live array + chat.messages = messages.map((message) => sanitizeForCloning(message)) chat.updatedAt = new Date() } From 83e6552d71476f78779d9340787ee4162d36ef15 Mon Sep 17 00:00:00 2001 From: Vaibhav <117663341+7ttp@users.noreply.github.com> Date: Mon, 20 Jul 2026 23:28:30 +0530 Subject: [PATCH 2/6] fix: preserve function responses (#47920) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - adds up to: https://github.com/supabase/cli/pull/5862 ## Summary by CodeRabbit * **New Features** * Added an “Error docs” link in Edge Function testing UI when an `sb-error-code` header is present. * **Bug Fixes** * Improved the Edge Function test proxy to consistently preserve upstream status, headers (including repeated headers), and response bodies without transformation. * Enhanced handling for invalid function URLs and upstream fetch failures. * **Tests** * Added unit, API, and Playwright E2E coverage for error docs linking and response proxy behavior. --- .../EdgeFunctionDetails.types.ts | 2 +- .../EdgeFunctionDetails.utils.tsx | 18 +++ .../EdgeFunctionTesterSheet.tsx | 32 +++- apps/studio/pages/api/edge-functions/test.ts | 43 ++---- .../EdgeFunctionDetails.utils.test.ts | 44 ++++++ .../pages/api/edge-functions/test.test.ts | 141 ++++++++++++++++++ e2e/studio/features/edge-functions.spec.ts | 61 ++++++++ 7 files changed, 301 insertions(+), 40 deletions(-) create mode 100644 apps/studio/tests/components/Functions/EdgeFunctionDetails.utils.test.ts create mode 100644 apps/studio/tests/pages/api/edge-functions/test.test.ts create mode 100644 e2e/studio/features/edge-functions.spec.ts diff --git a/apps/studio/components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionDetails.types.ts b/apps/studio/components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionDetails.types.ts index a831766cb30b8..d305bd8ccab19 100644 --- a/apps/studio/components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionDetails.types.ts +++ b/apps/studio/components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionDetails.types.ts @@ -1,6 +1,6 @@ export type ResponseData = { status: number - headers: Record + headers: Record body: string } diff --git a/apps/studio/components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionDetails.utils.tsx b/apps/studio/components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionDetails.utils.tsx index 25fe2a6cc0a2f..21b02412ecb1a 100644 --- a/apps/studio/components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionDetails.utils.tsx +++ b/apps/studio/components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionDetails.utils.tsx @@ -1,4 +1,7 @@ +import { getAnchor } from '@ui/components/CustomHTMLElements/CustomHTMLElements.utils' + import { EdgeFunction } from '@/data/edge-functions/edge-function-query' +import { DOCS_URL } from '@/lib/constants' export const generateCLICommands = ({ selectedFunction, @@ -99,3 +102,18 @@ export const generateCLICommands = ({ return { managementCommands, secretCommands, invokeCommands } } + +export const getEdgeFunctionErrorDocs = (headers: Record) => { + const header = Object.entries(headers).find( + ([name]) => name.toLowerCase() === 'sb-error-code' + )?.[1] + const code = (Array.isArray(header) ? header[0] : header)?.trim() + const anchor = code ? getAnchor(code) : undefined + + if (!code || !anchor) return undefined + + return { + code, + href: `${DOCS_URL}/guides/functions/error-codes#${anchor}`, + } +} diff --git a/apps/studio/components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionTesterSheet.tsx b/apps/studio/components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionTesterSheet.tsx index 250f5096474c9..1a719dc33da15 100644 --- a/apps/studio/components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionTesterSheet.tsx +++ b/apps/studio/components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionTesterSheet.tsx @@ -1,7 +1,7 @@ import { zodResolver } from '@hookform/resolvers/zod' import { PermissionAction } from '@supabase/shared-types/out/constants' import { useParams } from 'common' -import { Loader2, Plus, Send, X } from 'lucide-react' +import { BookOpen, Loader2, Plus, Send, X } from 'lucide-react' import { useState } from 'react' import { useFieldArray, useForm } from 'react-hook-form' import { @@ -37,6 +37,7 @@ import * as z from 'zod' import { HTTP_METHODS } from './EdgeFunctionDetails.constants' import { ErrorWithStatus, ResponseData } from './EdgeFunctionDetails.types' +import { getEdgeFunctionErrorDocs } from './EdgeFunctionDetails.utils' import { RoleImpersonationPopover } from '@/components/interfaces/RoleImpersonationSelector/RoleImpersonationPopover' import { ShortcutTooltip } from '@/components/ui/ShortcutTooltip' import { useAPIKeys } from '@/data/api-keys/api-keys-query' @@ -100,6 +101,7 @@ const EdgeFunctionTesterSheetContent = ({ visible, onClose }: EdgeFunctionTester const [response, setResponse] = useState(null) const [error, setError] = useState(null) + const errorDocs = response ? getEdgeFunctionErrorDocs(response.headers) : undefined const { can: canReadAPIKeys } = useAsyncCheckPermissions(PermissionAction.SECRETS_READ, '*') const { data: apiKeysData } = useAPIKeys({ projectRef }, { enabled: canReadAPIKeys }) @@ -423,12 +425,28 @@ const EdgeFunctionTesterSheetContent = ({ visible, onClose }: EdgeFunctionTester Headers - = 400 ? 'destructive' : 'success'} - className="-translate-y-1" - > - {response.status} - +
+ {errorDocs !== undefined && ( + + )} + = 400 ? 'destructive' : 'success'}> + {response.status} + +
= {} + const responseHeaders: Record = {} response.headers.forEach((value, key) => { - responseHeaders[key] = value + const existing = responseHeaders[key] + if (existing === undefined) { + responseHeaders[key] = value + } else if (Array.isArray(existing)) { + existing.push(value) + } else { + responseHeaders[key] = [existing, value] + } }) - return res.status(response.status).json({ + return res.status(200).json({ status: response.status, headers: responseHeaders, body: responseBody, diff --git a/apps/studio/tests/components/Functions/EdgeFunctionDetails.utils.test.ts b/apps/studio/tests/components/Functions/EdgeFunctionDetails.utils.test.ts new file mode 100644 index 0000000000000..af31ed7d99cd5 --- /dev/null +++ b/apps/studio/tests/components/Functions/EdgeFunctionDetails.utils.test.ts @@ -0,0 +1,44 @@ +import { describe, expect, it } from 'vitest' + +import { getEdgeFunctionErrorDocs } from '@/components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionDetails.utils' +import { DOCS_URL } from '@/lib/constants' + +describe('getEdgeFunctionErrorDocs', () => { + it('builds a documentation link from the error code header', () => { + expect( + getEdgeFunctionErrorDocs({ 'sb-error-code': 'UNAUTHORIZED_INVALID_JWT_FORMAT' }) + ).toEqual({ + code: 'UNAUTHORIZED_INVALID_JWT_FORMAT', + href: `${DOCS_URL}/guides/functions/error-codes#unauthorizedinvalidjwtformat`, + }) + }) + + it('uses the same anchor format as documentation headings', () => { + expect(getEdgeFunctionErrorDocs({ 'sb-error-code': 'WORKER_RESOURCE_LIMIT' })).toEqual({ + code: 'WORKER_RESOURCE_LIMIT', + href: `${DOCS_URL}/guides/functions/error-codes#workerresourcelimit`, + }) + }) + + it('matches the error code header case-insensitively', () => { + expect(getEdgeFunctionErrorDocs({ 'Sb-Error-Code': 'BOOT_ERROR' })).toEqual({ + code: 'BOOT_ERROR', + href: `${DOCS_URL}/guides/functions/error-codes#booterror`, + }) + }) + + it('uses the first value when the header contains multiple values', () => { + expect(getEdgeFunctionErrorDocs({ 'sb-error-code': ['WORKER_ERROR', 'BOOT_ERROR'] })).toEqual({ + code: 'WORKER_ERROR', + href: `${DOCS_URL}/guides/functions/error-codes#workererror`, + }) + }) + + it('omits the link when the error code header is missing', () => { + expect(getEdgeFunctionErrorDocs({})).toBeUndefined() + }) + + it.each(['', '___'])('omits the link for an unusable error code', (code) => { + expect(getEdgeFunctionErrorDocs({ 'sb-error-code': code })).toBeUndefined() + }) +}) diff --git a/apps/studio/tests/pages/api/edge-functions/test.test.ts b/apps/studio/tests/pages/api/edge-functions/test.test.ts new file mode 100644 index 0000000000000..a3fdf4b656d4a --- /dev/null +++ b/apps/studio/tests/pages/api/edge-functions/test.test.ts @@ -0,0 +1,141 @@ +import { createMocks } from 'node-mocks-http' +import { afterEach, describe, expect, it, vi } from 'vitest' + +import handler from '../../../../pages/api/edge-functions/test' + +vi.mock('common', () => ({ IS_PLATFORM: true })) + +const createRequest = (url = 'https://abcdefghijklmnopqrst.supabase.co/functions/v1/test') => + createMocks({ + method: 'POST', + body: { + url, + method: 'POST', + body: '{}', + headers: {}, + }, + }) + +describe('/api/edge-functions/test', () => { + afterEach(() => { + vi.unstubAllGlobals() + }) + + it('preserves unsuccessful edge function responses', async () => { + const body = JSON.stringify({ + code: 'UNAUTHORIZED_NO_AUTH_HEADER', + message: 'Missing authorization header', + }) + vi.stubGlobal( + 'fetch', + vi.fn().mockResolvedValue( + new Response(body, { + status: 401, + headers: { + 'content-type': 'application/json', + 'sb-error-code': 'UNAUTHORIZED_NO_AUTH_HEADER', + }, + }) + ) + ) + const { req, res } = createRequest() + + await handler(req, res) + + expect(res._getStatusCode()).toBe(200) + expect(JSON.parse(res._getData())).toEqual({ + status: 401, + headers: { + 'content-type': 'application/json', + 'sb-error-code': 'UNAUTHORIZED_NO_AUTH_HEADER', + }, + body, + }) + }) + + it('preserves multiple Set-Cookie headers', async () => { + const headers = new Headers() + headers.append('set-cookie', 'session=one; Path=/; HttpOnly') + headers.append('set-cookie', 'csrf=two; Path=/; SameSite=Lax') + vi.stubGlobal('fetch', vi.fn().mockResolvedValue(new Response(null, { headers }))) + const { req, res } = createRequest() + + await handler(req, res) + + expect(JSON.parse(res._getData())).toEqual({ + status: 200, + headers: { + 'set-cookie': ['session=one; Path=/; HttpOnly', 'csrf=two; Path=/; SameSite=Lax'], + }, + body: '', + }) + }) + + it.each([ + ['successful JSON', JSON.stringify({ ok: true }), 'application/json', 200], + [ + 'gateway JSON', + JSON.stringify({ message: 'Name resolution failed' }), + 'application/json', + 503, + ], + ['legacy JSON', JSON.stringify({ msg: 'Invalid JWT' }), 'application/json', 503], + [ + 'nested JSON', + JSON.stringify({ error: { message: 'Function failed' } }), + 'application/json', + 503, + ], + ['arbitrary JSON', JSON.stringify({ details: ['Function failed'] }), 'application/json', 503], + ['plain text', 'Bad Gateway', 'text/plain', 503], + ['malformed JSON', '{"message": invalid json', 'application/json', 503], + ])('preserves %s bodies without parsing them', async (_name, body, contentType, status) => { + vi.stubGlobal( + 'fetch', + vi.fn().mockResolvedValue( + new Response(body, { + status, + headers: { 'content-type': contentType }, + }) + ) + ) + const { req, res } = createRequest() + + await handler(req, res) + + expect(res._getStatusCode()).toBe(200) + expect(JSON.parse(res._getData())).toEqual({ + status, + headers: { 'content-type': contentType }, + body, + }) + }) + + it('rejects invalid URLs without making an upstream request', async () => { + const fetchMock = vi.fn() + vi.stubGlobal('fetch', fetchMock) + const { req, res } = createRequest('https://example.com/functions/v1/test') + + await handler(req, res) + + expect(fetchMock).not.toHaveBeenCalled() + expect(res._getStatusCode()).toBe(400) + expect(JSON.parse(res._getData())).toEqual({ + status: 400, + error: { message: 'Provided URL is not a valid Supabase edge function URL' }, + }) + }) + + it('returns fetch failures as proxy errors', async () => { + vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('Connection refused'))) + const { req, res } = createRequest() + + await handler(req, res) + + expect(res._getStatusCode()).toBe(500) + expect(JSON.parse(res._getData())).toEqual({ + status: 500, + error: { message: 'Connection refused' }, + }) + }) +}) diff --git a/e2e/studio/features/edge-functions.spec.ts b/e2e/studio/features/edge-functions.spec.ts new file mode 100644 index 0000000000000..8861185791eeb --- /dev/null +++ b/e2e/studio/features/edge-functions.spec.ts @@ -0,0 +1,61 @@ +import { expect } from '@playwright/test' + +import { test } from '../utils/test.js' +import { toUrl } from '../utils/to-url.js' + +const FUNCTION_SLUG = 'error-code-docs' + +test.describe('Edge Functions', () => { + test('links an sb-error-code response to its documentation', async ({ page, ref }) => { + await page.route(`**/api/v1/projects/${ref}/functions/${FUNCTION_SLUG}`, async (route) => { + const timestamp = Date.now() + await route.fulfill({ + contentType: 'application/json', + body: JSON.stringify({ + id: '00000000-0000-0000-0000-000000000000', + slug: FUNCTION_SLUG, + name: FUNCTION_SLUG, + version: 1, + status: 'ACTIVE', + entrypoint_path: `supabase/functions/${FUNCTION_SLUG}/index.ts`, + created_at: timestamp, + updated_at: timestamp, + }), + }) + }) + await page.route('**/api/edge-functions/test', async (route) => { + await route.fulfill({ + contentType: 'application/json', + body: JSON.stringify({ + status: 401, + headers: { 'sb-error-code': 'UNAUTHORIZED_INVALID_JWT_FORMAT' }, + body: '', + }), + }) + }) + + await page.goto(toUrl(`/project/${ref}/functions/${FUNCTION_SLUG}`)) + await page.getByRole('button', { name: 'Test', exact: true }).click() + + const testResponse = page.waitForResponse( + (response) => + response.url().includes('/api/edge-functions/test') && + response.request().method() === 'POST' + ) + await page.getByRole('button', { name: 'Send Request' }).click() + expect( + (await testResponse).ok(), + 'Studio proxy should return a successful response envelope' + ).toBeTruthy() + + await expect( + page.getByRole('link', { + name: 'View documentation for UNAUTHORIZED_INVALID_JWT_FORMAT (opens in new tab)', + }), + 'Error response should link to its documentation section' + ).toHaveAttribute( + 'href', + 'https://supabase.com/docs/guides/functions/error-codes#unauthorizedinvalidjwtformat' + ) + }) +}) From 45ba40eff937cdfd2fea7183c785db2124c4601f Mon Sep 17 00:00:00 2001 From: Pedro Rodrigues <44656907+Rodriguespn@users.noreply.github.com> Date: Mon, 20 Jul 2026 19:37:02 +0100 Subject: [PATCH 3/6] docs: add Kimi Code to MCP and AI coding agent plugin setup (#48099) Adds Kimi Code to the MCP server setup and AI coding agent plugins docs: ### MCP Server config image ### AI coding agent plugins image Closes AI-933 ## Summary by CodeRabbit * **New Features** * Added **Kimi Code** as a selectable plugin client in the plugins panel. * Added **Kimi-specific installation/setup instructions**, including guidance on placing `mcp.json`, confirming the trust prompt, and using `/plugins` plus `/mcp` and `/mcp-config`. * Extended the **MCP URL builder** to generate Kimi Code HTTP-based server configuration. * Included **Kimi** in the **IDE** client group with a dedicated **Kimi icon** for UI display. --------- Co-authored-by: Claude Opus 4.8 (1M context) --- apps/docs/features/ui/AgentPluginsPanel.tsx | 30 +++++++++++++++++++ .../McpUrlBuilder/assets/kimi-icon-dark.svg | 4 +++ .../src/McpUrlBuilder/assets/kimi-icon.svg | 4 +++ .../src/McpUrlBuilder/clients.data.ts | 21 ++++++++++++- .../McpUrlBuilder/clients.instructions.md.tsx | 19 ++++++++++++ .../ui-patterns/src/McpUrlBuilder/types.ts | 14 +++++++++ .../src/McpUrlBuilder/utils/mcpIconAssets.ts | 3 ++ 7 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 packages/ui-patterns/src/McpUrlBuilder/assets/kimi-icon-dark.svg create mode 100644 packages/ui-patterns/src/McpUrlBuilder/assets/kimi-icon.svg diff --git a/apps/docs/features/ui/AgentPluginsPanel.tsx b/apps/docs/features/ui/AgentPluginsPanel.tsx index 33518664e6daf..40de6553a1c30 100644 --- a/apps/docs/features/ui/AgentPluginsPanel.tsx +++ b/apps/docs/features/ui/AgentPluginsPanel.tsx @@ -50,6 +50,14 @@ const PLUGIN_CLIENTS: PluginClient[] = [ docsUrl: 'https://docs.github.com/en/copilot/how-tos/copilot-cli/customize-copilot/plugins-finding-installing', }, + { + key: 'kimi', + label: 'Kimi Code', + icon: 'kimi', + hasDistinctDarkIcon: true, + repoUrl: 'https://github.com/supabase-community/supabase-plugin', + docsUrl: 'https://www.kimi.com/code/docs/en/kimi-code-cli/customization/plugins.html', + }, ] function PluginInstructions({ client }: { client: PluginClient }) { @@ -172,6 +180,28 @@ function PluginInstructions({ client }: { client: PluginClient }) { ) } + if (client.key === 'kimi') { + return ( +
+

Open Kimi Code by running

+ +

+ Then install the Supabase plugin from within the session: +

+ +

+ Confirm the trust prompt to install. Kimi adds the plugin to its native plugin store. Run{' '} + /plugins at any time to view or reload installed plugins. +

+
+ ) + } + if (client.key === 'github-copilot') { return (
diff --git a/packages/ui-patterns/src/McpUrlBuilder/assets/kimi-icon-dark.svg b/packages/ui-patterns/src/McpUrlBuilder/assets/kimi-icon-dark.svg new file mode 100644 index 0000000000000..949cf16d79207 --- /dev/null +++ b/packages/ui-patterns/src/McpUrlBuilder/assets/kimi-icon-dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/ui-patterns/src/McpUrlBuilder/assets/kimi-icon.svg b/packages/ui-patterns/src/McpUrlBuilder/assets/kimi-icon.svg new file mode 100644 index 0000000000000..209edf3b9dac6 --- /dev/null +++ b/packages/ui-patterns/src/McpUrlBuilder/assets/kimi-icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/ui-patterns/src/McpUrlBuilder/clients.data.ts b/packages/ui-patterns/src/McpUrlBuilder/clients.data.ts index 06e5dd64f0b27..f1bfecd348ada 100644 --- a/packages/ui-patterns/src/McpUrlBuilder/clients.data.ts +++ b/packages/ui-patterns/src/McpUrlBuilder/clients.data.ts @@ -6,6 +6,7 @@ import type { FactoryMcpConfig, GeminiMcpConfig, GooseMcpConfig, + KimiMcpConfig, McpClientBaseConfig, McpClientConfig, McpClientDeepLinkOptions, @@ -152,6 +153,24 @@ export const MCP_CLIENT_DATA: McpClientData[] = [ } }, }, + { + key: 'kimi', + label: 'Kimi Code', + icon: 'kimi', + hasDistinctDarkIcon: true, + configFile: '.kimi-code/mcp.json', + externalDocsUrl: 'https://www.kimi.com/code/docs/en/kimi-code-cli/customization/mcp.html', + transformConfig: (config): KimiMcpConfig => { + return { + mcpServers: { + supabase: { + transport: 'http', + url: config.mcpServers.supabase.url, + }, + }, + } + }, + }, { key: 'gemini-cli', label: 'Gemini CLI', @@ -375,7 +394,7 @@ export const MCP_CLIENT_GROUPS = [ }, { heading: 'IDE', - keys: ['cursor', 'vscode', 'antigravity', 'kiro', 'windsurf'], + keys: ['cursor', 'vscode', 'antigravity', 'kiro', 'windsurf', 'kimi'], }, ] as const diff --git a/packages/ui-patterns/src/McpUrlBuilder/clients.instructions.md.tsx b/packages/ui-patterns/src/McpUrlBuilder/clients.instructions.md.tsx index be200091d4e10..670b09ceadede 100644 --- a/packages/ui-patterns/src/McpUrlBuilder/clients.instructions.md.tsx +++ b/packages/ui-patterns/src/McpUrlBuilder/clients.instructions.md.tsx @@ -88,6 +88,25 @@ export const MCP_CLIENT_INSTRUCTIONS: Record = { ), }, + kimi: { + alternate: () => ( + <> + + Kimi Code reads from your current working + directory. To make the server available in every project, place the file under{' '} + instead, which defaults to{' '} + . + + + Restart Kimi Code or start a new session to load the server, then check its status by + running: + + + To configure MCP servers and complete the Supabase OAuth login, run: + + + ), + }, 'gemini-cli': { primary: ({ isPlatform, url }) => ( <> diff --git a/packages/ui-patterns/src/McpUrlBuilder/types.ts b/packages/ui-patterns/src/McpUrlBuilder/types.ts index 3b19577127071..97a03ae103b4d 100644 --- a/packages/ui-patterns/src/McpUrlBuilder/types.ts +++ b/packages/ui-patterns/src/McpUrlBuilder/types.ts @@ -175,6 +175,19 @@ export interface CopilotMcpConfig extends McpClientBaseConfig { } } +/** + * Configuration format for Kimi Code CLI MCP client. + * Kimi keys the server transport with `transport` (not `type`). + */ +export interface KimiMcpConfig extends McpClientBaseConfig { + mcpServers: { + supabase: { + transport: 'http' + url: string + } + } +} + // Union of all possible config types export type McpClientConfig = | AntigravityMcpConfig @@ -186,6 +199,7 @@ export type McpClientConfig = | FactoryMcpConfig | GeminiMcpConfig | GooseMcpConfig + | KimiMcpConfig | McpClientBaseConfig | OpenCodeMcpConfig | OtherMcpConfig diff --git a/packages/ui-patterns/src/McpUrlBuilder/utils/mcpIconAssets.ts b/packages/ui-patterns/src/McpUrlBuilder/utils/mcpIconAssets.ts index ebebe51289ba1..cb27dd3fd299f 100644 --- a/packages/ui-patterns/src/McpUrlBuilder/utils/mcpIconAssets.ts +++ b/packages/ui-patterns/src/McpUrlBuilder/utils/mcpIconAssets.ts @@ -11,6 +11,8 @@ import factoryIcon from '../assets/factory-icon.svg' import geminiCliIcon from '../assets/gemini-cli-icon.svg' import gooseDarkIcon from '../assets/goose-icon-dark.svg' import gooseIcon from '../assets/goose-icon.svg' +import kimiDarkIcon from '../assets/kimi-icon-dark.svg' +import kimiIcon from '../assets/kimi-icon.svg' import kiroIcon from '../assets/kiro-icon.svg' import openaiDarkIcon from '../assets/openai-icon-dark.svg' import openaiIcon from '../assets/openai-icon.svg' @@ -37,6 +39,7 @@ const MCP_CLIENT_ICON_ASSETS = { factory: { light: factoryIcon, dark: factoryDarkIcon }, 'gemini-cli': { light: geminiCliIcon, dark: geminiCliIcon }, goose: { light: gooseIcon, dark: gooseDarkIcon }, + kimi: { light: kimiIcon, dark: kimiDarkIcon }, kiro: { light: kiroIcon, dark: kiroIcon }, openai: { light: openaiIcon, dark: openaiDarkIcon }, opencode: { light: opencodeIcon, dark: opencodeDarkIcon }, From ceb411056895d38665269d5afbe5cfe5d8a32587 Mon Sep 17 00:00:00 2001 From: Pedro Rodrigues <44656907+Rodriguespn@users.noreply.github.com> Date: Mon, 20 Jul 2026 20:36:24 +0100 Subject: [PATCH 4/6] docs: add VS Code to the AI coding agent plugin install picker (#48108) Adds VS Code to the AI coding agent plugins docs, as VS Code now [supports plugins](https://code.visualstudio.com/docs/agent-customization/agent-plugins) image ### Steps to reproduce it 1. Navigate to https://docs-git-add-vs-code-plugins-support-to-docs-supabase.vercel.app/docs/guides/ai-tools/plugins#manual-install 2. Select "VS Code" in the `Client` dropdown ## Summary by CodeRabbit * **New Features** * Added Visual Studio Code to the supported plugin integrations. * Added VS Code installation guidance, including the plugin repository and manifest specification links. --- apps/docs/features/ui/AgentPluginsPanel.tsx | 37 +++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/apps/docs/features/ui/AgentPluginsPanel.tsx b/apps/docs/features/ui/AgentPluginsPanel.tsx index 40de6553a1c30..c8b992a9967da 100644 --- a/apps/docs/features/ui/AgentPluginsPanel.tsx +++ b/apps/docs/features/ui/AgentPluginsPanel.tsx @@ -58,6 +58,13 @@ const PLUGIN_CLIENTS: PluginClient[] = [ repoUrl: 'https://github.com/supabase-community/supabase-plugin', docsUrl: 'https://www.kimi.com/code/docs/en/kimi-code-cli/customization/plugins.html', }, + { + key: 'vscode', + label: 'VS Code', + icon: 'vscode', + repoUrl: 'https://github.com/supabase-community/supabase-plugin', + docsUrl: 'https://code.visualstudio.com/docs/agent-customization/agent-plugins', + }, ] function PluginInstructions({ client }: { client: PluginClient }) { @@ -202,6 +209,36 @@ function PluginInstructions({ client }: { client: PluginClient }) { ) } + if (client.key === 'vscode') { + return ( +
+

+ Open the Command Palette (Cmd/Ctrl+Shift+P) and run{' '} + Chat: Install Plugin From Source, then paste the Supabase plugin + repository URL: +

+ +

+ VS Code auto-detects the vendor-neutral{' '} + + Open Plugin + {' '} + manifest in the repo. Review the trust prompt to finish installing. +

+
+ ) + } + if (client.key === 'github-copilot') { return (
From 4d9c37086d9be5e00ecbad73963660388dcdc1a9 Mon Sep 17 00:00:00 2001 From: Francesco Sansalvadore Date: Mon, 20 Jul 2026 23:41:35 +0200 Subject: [PATCH 5/6] fix(docs): broken link (#48117) Fix broken "Dashboard integrations" link with missing `/docs/` in the [Partner Catalog docs page](https://docs-git-fix-docs-dashboard-integrations-link-supabase.vercel.app/docs/guides/integrations/partner-catalog). ## Summary by CodeRabbit * **Documentation** * Clarified the difference between the Partner Catalog and Dashboard Integrations. * Explained that Dashboard Integrations are installed directly from a Supabase project in the dashboard. * Updated the Dashboard Integrations link. --- apps/docs/content/guides/integrations/partner-catalog.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/docs/content/guides/integrations/partner-catalog.mdx b/apps/docs/content/guides/integrations/partner-catalog.mdx index e6e59da9cfb6d..aafe1bf5b84f5 100644 --- a/apps/docs/content/guides/integrations/partner-catalog.mdx +++ b/apps/docs/content/guides/integrations/partner-catalog.mdx @@ -6,7 +6,7 @@ description: 'Integrations and Partners' The [Partner Catalog](/partners/catalog) is Supabase's public directory of third-party integrations. It lists tools that extend your Supabase project. These tools cover Auth, Caching, Hosting, and Low-code categories. -The Partner Catalog is different from [Dashboard Integrations](/guides/integrations#dashboard-integrations). You install Dashboard Integrations directly from a project in the Supabase Dashboard. +The Partner Catalog is different from [Dashboard Integrations](/docs/guides/integrations#dashboard-integrations). You install Dashboard Integrations directly from a project in the Supabase Dashboard. ## Build an integration From 37296128e07a1810c00a5a6130074f3fc3269c79 Mon Sep 17 00:00:00 2001 From: Danny White <3104761+dnywh@users.noreply.github.com> Date: Mon, 20 Jul 2026 18:00:24 -0400 Subject: [PATCH 6/6] fix(studio): untangle nested buttons in editor tabs (#48101) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What kind of change does this PR introduce? A11y markup + keyboard close for editor tabs (Table Editor open tables / SQL Editor snippets). ## What is the current behavior? Editor tabs nest interactive elements (already in prod): 1. Sortable shell spreads dnd-kit `attributes` → `div role="button" tabindex="0"` 2. Inner `TabsTrigger` → real ` +
{index < openTabs.length && (
)} diff --git a/apps/studio/components/layouts/Tabs/Tabs.tsx b/apps/studio/components/layouts/Tabs/Tabs.tsx index c1959e98717aa..72b06e913ac31 100644 --- a/apps/studio/components/layouts/Tabs/Tabs.tsx +++ b/apps/studio/components/layouts/Tabs/Tabs.tsx @@ -214,27 +214,40 @@ export const EditorTabs = () => { {/* Non-draggable new tab */} {hasNewTab && ( - - -
- New -
- + { + if (e.key !== 'Delete' && e.key !== 'Backspace') return + e.preventDefault() + e.stopPropagation() + handleClose('new') + }} + className={cn( + 'flex items-center gap-2 px-3 text-xs', + 'bg-dash-sidebar/50 dark:bg-surface-100/50', + 'data-[state=active]:bg-dash-sidebar dark:data-[state=active]:bg-surface-100', + 'relative group h-full border-t-2 border-b-0!', + 'hover:bg-surface-300 dark:hover:bg-surface-100' + )} + > + +
+ New +
+ {/* Reserve close-icon width; close is a sibling overlay. */} + +
+ + +
)}