From de4bec77d6a9310cc37100182e12790a0428aad6 Mon Sep 17 00:00:00 2001 From: Alaister Young Date: Wed, 19 Aug 2026 21:03:39 +0800 Subject: [PATCH 1/7] [MUL-1338] fix(studio): lock compute size to large for HA projects (#49249) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the High availability (Multigres) toggle is enabled in the New Project form, the Compute size dropdown now offers only **Large** and the form value is forced to `large`. Previously HA projects showed the same micro/small/medium options as regular projects. **Added:** - `HIGH_AVAILABILITY_INSTANCE_SIZE` constant (`'large'`) alongside the other `HIGH_AVAILABILITY_*` constants **Changed:** - `ComputeSizeSelector` watches `highAvailability` and renders only Large when it's on (hiding the "Larger instance sizes available after creation" row); the `cloudProvider` read is now a reactive `useWatch` instead of a render-time `getValues()`, so the list re-filters when HA forces the provider to `AWS_K8S` - `HighAvailabilityInput` forces `instanceSize` to `large` when HA toggles on and restores the previously selected size when it toggles off, alongside the existing `dbRegion`/`cloudProvider` handling - The compute size and region selects ignore Radix's spurious `onValueChange('')` — Radix emits it when a select's value and option list change in the same tick, which wiped the forced value (details in the inline comments) - HA projects skip the "Confirm compute costs" modal on submit — HA is free during Alpha, so the forced large size shouldn't trigger the $110/mo confirmation ## To test - On a paid org, open the New Project form: with HA off, the Compute size dropdown shows micro/small/medium plus the disabled "Larger instance sizes available after creation" row - Toggle High availability on: the dropdown shows only Large, the trigger reads "large / 8 GB RAM / 2-core CPU" (not the placeholder), the region locks as before, and the footer shows $110/m - Check the network tab: the `available-regions` request goes out with `desired_instance_size=large` and returns 200 (no request with an empty `desired_instance_size`) - Select medium first, toggle HA on then off: medium is restored (same for other sizes); rapid toggling shouldn't leave the field blank - With HA on, submitting goes straight through without the "Confirm compute costs" modal; a non-HA medium project still shows it ## Summary by CodeRabbit * **New Features** * High-availability projects now automatically use the required dedicated instance size. * Disabling high availability restores the previously selected instance size. * Compute size options are filtered based on cloud provider and high-availability settings. * **Bug Fixes** * Prevented accidental clearing of compute size or region selections during option updates. * Compute-cost confirmation is no longer required for high-availability projects. --------- Co-authored-by: Alaister Young <10985857+alaister@users.noreply.github.com> --- .../ProjectCreation/ComputeSizeSelector.tsx | 112 ++++++++++-------- .../ProjectCreation/HighAvailabilityInput.tsx | 14 +++ .../ProjectCreation.constants.ts | 1 + .../ProjectCreation.utils.test.ts | 4 +- .../ProjectCreation/ProjectCreationForm.tsx | 9 +- .../ProjectCreation/RegionSelector.tsx | 6 +- 6 files changed, 93 insertions(+), 53 deletions(-) diff --git a/apps/studio/components/interfaces/ProjectCreation/ComputeSizeSelector.tsx b/apps/studio/components/interfaces/ProjectCreation/ComputeSizeSelector.tsx index 49ab883d35f35..393d407d52de3 100644 --- a/apps/studio/components/interfaces/ProjectCreation/ComputeSizeSelector.tsx +++ b/apps/studio/components/interfaces/ProjectCreation/ComputeSizeSelector.tsx @@ -1,3 +1,4 @@ +import { useMemo } from 'react' import { UseFormReturn } from 'react-hook-form' import { CloudProvider } from 'shared-data' import { @@ -8,11 +9,12 @@ import { SelectItem, SelectTrigger, SelectValue, + useWatch, } from 'ui' import { ComputeBadge } from 'ui-patterns/ComputeBadge' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' -import { sizes } from './ProjectCreation.constants' +import { HIGH_AVAILABILITY_INSTANCE_SIZE, sizes } from './ProjectCreation.constants' import { CreateProjectForm } from './ProjectCreation.schema' import { InlineLink } from '@/components/ui/InlineLink' import Panel from '@/components/ui/Panel' @@ -24,6 +26,19 @@ interface ComputeSizeSelectorProps { } export const ComputeSizeSelector = ({ form }: ComputeSizeSelectorProps) => { + const cloudProvider = useWatch({ control: form.control, name: 'cloudProvider' }) as CloudProvider + const highAvailability = useWatch({ control: form.control, name: 'highAvailability' }) + + const sizeOptions = useMemo( + () => + highAvailability + ? [HIGH_AVAILABILITY_INSTANCE_SIZE] + : sizes.filter((option) => + instanceSizeSpecs[option].cloud_providers.includes(cloudProvider) + ), + [highAvailability, cloudProvider] + ) + return ( { layout="horizontal" label="Compute size" description={ - <> -

- The size for your dedicated database. You can change this later. Learn more about{' '} - - compute add-ons - {' '} - and{' '} - - compute billing - - . -

- +

+ The size for your dedicated database. You can change this later. Learn more about{' '} + + compute add-ons + {' '} + and{' '} + + compute billing + + . +

} > - value !== '' && field.onChange(value)} + > { - {sizes - .filter((option) => - instanceSizeSpecs[option].cloud_providers.includes( - form.getValues('cloudProvider') as CloudProvider - ) - ) - .map((option) => { - return ( - -
-
- -
+ {sizeOptions.map((option) => { + return ( + +
+
+ +
-
- - {instanceSizeSpecs[option].ram} RAM /{' '} - {instanceSizeSpecs[option].cpu} CPU - -

- ${instanceSizeSpecs[option].priceHourly}/hour (~$ - {instanceSizeSpecs[option].priceMonthly}/month) -

-
+
+ + {instanceSizeSpecs[option].ram} RAM / {instanceSizeSpecs[option].cpu}{' '} + CPU + +

+ ${instanceSizeSpecs[option].priceHourly}/hour (~$ + {instanceSizeSpecs[option].priceMonthly}/month) +

- - ) - })} - -
- Larger instance sizes available after creation -
-
+
+
+ ) + })} + {!highAvailability && ( + +
+ Larger instance sizes available after creation +
+
+ )} diff --git a/apps/studio/components/interfaces/ProjectCreation/HighAvailabilityInput.tsx b/apps/studio/components/interfaces/ProjectCreation/HighAvailabilityInput.tsx index 93b8e7ad0f810..9b619fe1ee83a 100644 --- a/apps/studio/components/interfaces/ProjectCreation/HighAvailabilityInput.tsx +++ b/apps/studio/components/interfaces/ProjectCreation/HighAvailabilityInput.tsx @@ -4,6 +4,7 @@ import { type CloudProvider } from 'shared-data' import { Badge, FormControl, FormField, Switch, useWatch } from 'ui' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' +import { HIGH_AVAILABILITY_INSTANCE_SIZE } from './ProjectCreation.constants' import { CreateProjectForm } from './ProjectCreation.schema' import Panel from '@/components/ui/Panel' import { useCheckEntitlements } from '@/hooks/misc/useCheckEntitlements' @@ -28,10 +29,12 @@ export const HighAvailabilityInput = ({ cloudProvider: CloudProvider | undefined postgresVersionSelection: string | undefined dbRegion: string | null + instanceSize: string | null }>({ cloudProvider: undefined, postgresVersionSelection: undefined, dbRegion: null, + instanceSize: null, }) const handleHighAvailabilityChange = (checked: boolean) => { @@ -55,6 +58,12 @@ export const HighAvailabilityInput = ({ beforeHighAvailability.current.dbRegion = currentRegion ?? null setValue('dbRegion', highAvailabilityRegionName) } + + const currentInstanceSize = getValues('instanceSize') + if (currentInstanceSize !== HIGH_AVAILABILITY_INSTANCE_SIZE) { + beforeHighAvailability.current.instanceSize = currentInstanceSize ?? null + setValue('instanceSize', HIGH_AVAILABILITY_INSTANCE_SIZE) + } } else { if (beforeHighAvailability.current.cloudProvider !== undefined) { setValue('cloudProvider', beforeHighAvailability.current.cloudProvider) @@ -73,6 +82,11 @@ export const HighAvailabilityInput = ({ setValue('dbRegion', beforeHighAvailability.current.dbRegion) beforeHighAvailability.current.dbRegion = null } + + if (beforeHighAvailability.current.instanceSize !== null) { + setValue('instanceSize', beforeHighAvailability.current.instanceSize) + beforeHighAvailability.current.instanceSize = null + } } } diff --git a/apps/studio/components/interfaces/ProjectCreation/ProjectCreation.constants.ts b/apps/studio/components/interfaces/ProjectCreation/ProjectCreation.constants.ts index fb6dcd1d258a7..6eeb886ce0f31 100644 --- a/apps/studio/components/interfaces/ProjectCreation/ProjectCreation.constants.ts +++ b/apps/studio/components/interfaces/ProjectCreation/ProjectCreation.constants.ts @@ -6,6 +6,7 @@ import type { export const HIGH_AVAILABILITY_POSTGRES_ENGINE = '17' satisfies PostgresEngine export const HIGH_AVAILABILITY_RELEASE_CHANNEL = 'ga' satisfies ReleaseChannel +export const HIGH_AVAILABILITY_INSTANCE_SIZE: DesiredInstanceSize = 'large' // [Joshen] Obtained from https://gist.github.com/tadast/8827699 export const COUNTRY_LAT_LON = { diff --git a/apps/studio/components/interfaces/ProjectCreation/ProjectCreation.utils.test.ts b/apps/studio/components/interfaces/ProjectCreation/ProjectCreation.utils.test.ts index ec08c1218ef8a..4f06720b287e3 100644 --- a/apps/studio/components/interfaces/ProjectCreation/ProjectCreation.utils.test.ts +++ b/apps/studio/components/interfaces/ProjectCreation/ProjectCreation.utils.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from 'vitest' import { + HIGH_AVAILABILITY_INSTANCE_SIZE, HIGH_AVAILABILITY_POSTGRES_ENGINE, HIGH_AVAILABILITY_RELEASE_CHANNEL, } from './ProjectCreation.constants' @@ -10,9 +11,10 @@ import { } from './ProjectCreation.utils' describe('High Availability project creation constraints', () => { - it('pins the Alpha Postgres engine and release channel', () => { + it('pins the Alpha Postgres engine, release channel, and compute size', () => { expect(HIGH_AVAILABILITY_POSTGRES_ENGINE).toBe('17') expect(HIGH_AVAILABILITY_RELEASE_CHANNEL).toBe('ga') + expect(HIGH_AVAILABILITY_INSTANCE_SIZE).toBe('large') }) it.each([ diff --git a/apps/studio/components/interfaces/ProjectCreation/ProjectCreationForm.tsx b/apps/studio/components/interfaces/ProjectCreation/ProjectCreationForm.tsx index 6ef98e5749da5..7b8b4639dc857 100644 --- a/apps/studio/components/interfaces/ProjectCreation/ProjectCreationForm.tsx +++ b/apps/studio/components/interfaces/ProjectCreation/ProjectCreationForm.tsx @@ -371,7 +371,14 @@ export const ProjectCreationForm = ({ values.instanceSize && !sizesWithNoCostConfirmationRequired.includes(values.instanceSize as DesiredInstanceSize) - if (additionalMonthlySpend > 0 && (hasOAuthApps || launchingLargerInstance)) { + // High availability projects are free during Alpha, so the forced large compute + // doesn't incur the usual compute costs. + const requiresCostConfirmation = + !values.highAvailability && + additionalMonthlySpend > 0 && + (hasOAuthApps || launchingLargerInstance) + + if (requiresCostConfirmation) { track('project_creation_simple_version_confirm_modal_opened', { instanceSize: values.instanceSize, }) diff --git a/apps/studio/components/interfaces/ProjectCreation/RegionSelector.tsx b/apps/studio/components/interfaces/ProjectCreation/RegionSelector.tsx index 79915d4e6f11c..4dbd2b49003ff 100644 --- a/apps/studio/components/interfaces/ProjectCreation/RegionSelector.tsx +++ b/apps/studio/components/interfaces/ProjectCreation/RegionSelector.tsx @@ -231,7 +231,11 @@ export const RegionSelector = ({ } > - value !== '' && field.onChange(value)} + disabled={isLoading} + > Date: Wed, 19 Aug 2026 07:51:56 -0600 Subject: [PATCH 2/7] fix(studio): only show restore completion once the restore has run (#48948) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolves [FE-4144](https://linear.app/supabase/issue/FE-4144/restore-flow-shows-completion-before-restore-is-actually-done) ## Problem `RestoringState` treated any `ACTIVE_HEALTHY` reading from the project status endpoint as "restore finished". Right after a restore is triggered the backend still reports the pre-restore status, so the first poll could land on `ACTIVE_HEALTHY` and flip the UI to "Restoration complete!" seconds into a restore that had barely started. `isCompleted` was local state nothing reset and polling stopped on that first reading, so the screen never self-corrected — "Return to project" then hung until a manual refresh. ## Changes - Gate completion on having observed the project leave the healthy state, so a stale pre-restore reading is no longer mistaken for a finished restore. - Keep polling through an unconfirmed healthy reading instead of stopping on it. - `onConfirm` clears its loading flag rather than relying on the layout to unmount the component. - Component tests covering both the premature completion and the stuck button. ## Needs validation Not yet verified against a real restore — please confirm on staging before merging. Worth checking in particular that a restore which completes normally still reaches the completion screen. There is one residual edge case left in place deliberately: if the details endpoint reports `RESTORING` while the status endpoint reports `ACTIVE_HEALTHY`, the UI now stays on "Restoration in progress" until the details query catches up. Fixing that properly needs an authoritative "restore initiated at" timestamp from the API, which does not exist today. ## Summary by CodeRabbit - **Bug Fixes** - Improved project restoration tracking to prevent completion from being reported prematurely. - Restoration now correctly detects failures and stops polling when appropriate. - Restore status and saved transition information are cleared after successful completion or failure. - Confirmation actions now remain reliable while project details refresh. - Restoring controls become usable again after the process finishes. - Improved the restore menu trigger behavior for more consistent interaction. --------- Co-authored-by: Alaister Young <10985857+alaister@users.noreply.github.com> --- .../ProjectLayout/RestoreFailedState.tsx | 2 +- .../ProjectLayout/RestoringState.test.tsx | 178 ++++++++++++++++++ .../layouts/ProjectLayout/RestoringState.tsx | 66 ++++--- 3 files changed, 219 insertions(+), 27 deletions(-) create mode 100644 apps/studio/components/layouts/ProjectLayout/RestoringState.test.tsx diff --git a/apps/studio/components/layouts/ProjectLayout/RestoreFailedState.tsx b/apps/studio/components/layouts/ProjectLayout/RestoreFailedState.tsx index 2855c96f40aa4..ade2b7c85b0b3 100644 --- a/apps/studio/components/layouts/ProjectLayout/RestoreFailedState.tsx +++ b/apps/studio/components/layouts/ProjectLayout/RestoreFailedState.tsx @@ -120,7 +120,7 @@ export const RestoreFailedState = () => { - +
-
From d3146a17550b99f90df0615bb4faf2992f152693 Mon Sep 17 00:00:00 2001 From: Pedro Rodrigues <44656907+Rodriguespn@users.noreply.github.com> Date: Wed, 19 Aug 2026 15:18:45 +0100 Subject: [PATCH 3/7] docs: add Grok plugin and MCP install instructions (#49212) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What this does Adds **Grok** (Grok Build) across the Supabase AI-tools docs, and fixes two logo gaps. - **Plugin docs** (`AgentPluginsPanel`) — Grok client + `grok plugin install …` / in-session `/plugins` steps. - **MCP docs** (`McpUrlBuilder`) — Grok under "AI Agent CLI": `~/.grok/config.toml` (`[mcp_servers.supabase]`), `grok mcp add … --transport http`, OAuth steps. - **"Pick your agent" grid** — add the Grok logo, and fix **Warp**'s pre-existing missing logo (both were absent from the grid's `ICON_ASSETS` map). - **Fix**: the plugins-page Cursor entry was missing `hasDistinctDarkIcon`, so its dark-mode logo fell back to the light mark — aligned with the MCP list. - Adds Grok + Warp agent logos (light + dark). ## Testing Verified against grok `1.0.5`: `grok plugin install …` works; the generated `config.toml` and `grok mcp add` command are both parsed by `grok mcp list`. Pairs with supabase-community/supabase-plugin#45 (the `.grok-plugin` surface); merge after that lands. ## Preview [Agent Plugin page](https://docs-git-pedrorodrigues-ai-932-add-grok-agent-p-12402b-supabase.vercel.app/docs/guides/ai-tools/plugins#manual-installation) image [MCP page](https://docs-git-pedrorodrigues-ai-932-add-grok-agent-p-12402b-supabase.vercel.app/docs/guides/ai-tools/mcp#remote-mcp-installation) image [AI Tools main page](https://docs-git-pedrorodrigues-ai-932-add-grok-agent-p-12402b-supabase.vercel.app/docs/guides/ai-tools#pick-your-agent) image Closes AI-932, AI-974 ## Summary by CodeRabbit ## New Features - Added Grok as a supported AI tool and MCP client. - Added Grok installation instructions, CLI setup, authentication, and connection verification guidance. - Added Grok icons for light and dark themes. - Added support for custom documentation link text in plugin panels. - Added Warp to the available icon assets. --------- Co-authored-by: Claude Opus 4.8 (1M context) --- .../data/content-listings/ai-tools.data.ts | 2 ++ .../features/ui/AgentPluginsPanel.data.ts | 11 +++++++++ apps/docs/features/ui/AgentPluginsPanel.tsx | 22 +++++++++++++++++- .../img/icons/agent-grok-icon-light.svg | 4 ++++ .../docs/public/img/icons/agent-grok-icon.svg | 4 ++++ .../img/icons/agent-warp-icon-light.svg | 3 +++ .../docs/public/img/icons/agent-warp-icon.svg | 3 +++ .../McpUrlBuilder/assets/grok-icon-dark.svg | 4 ++++ .../src/McpUrlBuilder/assets/grok-icon.svg | 4 ++++ .../src/McpUrlBuilder/clients.data.ts | 23 ++++++++++++++++++- .../McpUrlBuilder/clients.instructions.md.tsx | 22 ++++++++++++++++++ .../ui-patterns/src/McpUrlBuilder/types.ts | 14 +++++++++++ .../src/McpUrlBuilder/utils/mcpIconAssets.ts | 3 +++ 13 files changed, 117 insertions(+), 2 deletions(-) create mode 100644 apps/docs/public/img/icons/agent-grok-icon-light.svg create mode 100644 apps/docs/public/img/icons/agent-grok-icon.svg create mode 100644 apps/docs/public/img/icons/agent-warp-icon-light.svg create mode 100644 apps/docs/public/img/icons/agent-warp-icon.svg create mode 100644 packages/ui-patterns/src/McpUrlBuilder/assets/grok-icon-dark.svg create mode 100644 packages/ui-patterns/src/McpUrlBuilder/assets/grok-icon.svg diff --git a/apps/docs/data/content-listings/ai-tools.data.ts b/apps/docs/data/content-listings/ai-tools.data.ts index 6b4bf347d78d5..4b555dc87bc7f 100644 --- a/apps/docs/data/content-listings/ai-tools.data.ts +++ b/apps/docs/data/content-listings/ai-tools.data.ts @@ -15,12 +15,14 @@ const ICON_ASSETS: Record = { 'claude-code': { icon: '/docs/img/icons/agent-claude-icon', hasLightIcon: false }, codex: { icon: '/docs/img/icons/agent-openai-icon', hasLightIcon: true }, cursor: { icon: '/docs/img/icons/agent-cursor-icon', hasLightIcon: true }, + grok: { icon: '/docs/img/icons/agent-grok-icon', hasLightIcon: true }, 'gemini-cli': { icon: '/docs/img/icons/agent-gemini-cli-icon', hasLightIcon: false }, 'github-copilot': { icon: '/docs/img/icons/agent-copilot-icon', hasLightIcon: true }, kimi: { icon: '/docs/img/icons/agent-kimi-icon', hasLightIcon: true }, vscode: { icon: '/docs/img/icons/agent-vscode-icon', hasLightIcon: false }, antigravity: { icon: '/docs/img/icons/agent-antigravity-icon', hasLightIcon: false }, windsurf: { icon: '/docs/img/icons/agent-devin-icon', hasLightIcon: true }, + warp: { icon: '/docs/img/icons/agent-warp-icon', hasLightIcon: true }, goose: { icon: '/docs/img/icons/agent-goose-icon', hasLightIcon: true }, factory: { icon: '/docs/img/icons/agent-factory-icon', hasLightIcon: true }, opencode: { icon: '/docs/img/icons/agent-opencode-icon', hasLightIcon: true }, diff --git a/apps/docs/features/ui/AgentPluginsPanel.data.ts b/apps/docs/features/ui/AgentPluginsPanel.data.ts index 7678b94d615f2..91902ae641271 100644 --- a/apps/docs/features/ui/AgentPluginsPanel.data.ts +++ b/apps/docs/features/ui/AgentPluginsPanel.data.ts @@ -3,6 +3,7 @@ import type { McpClient } from 'ui-patterns/McpUrlBuilder' export interface PluginClient extends McpClient { repoUrl?: string docsUrl?: string + docsLinkText?: string } export const PLUGIN_CLIENTS: PluginClient[] = [ @@ -25,6 +26,7 @@ export const PLUGIN_CLIENTS: PluginClient[] = [ key: 'cursor', label: 'Cursor', icon: 'cursor', + hasDistinctDarkIcon: true, repoUrl: 'https://github.com/supabase-community/cursor-plugin', docsUrl: 'https://cursor.com/docs/plugins', }, @@ -44,6 +46,15 @@ export const PLUGIN_CLIENTS: PluginClient[] = [ docsUrl: 'https://docs.github.com/en/copilot/how-tos/copilot-cli/customize-copilot/plugins-finding-installing', }, + { + key: 'grok', + label: 'Grok', + icon: 'grok', + hasDistinctDarkIcon: true, + repoUrl: 'https://github.com/supabase-community/supabase-plugin', + docsUrl: 'https://docs.x.ai/build/features/skills-plugins-marketplaces#plugins', + docsLinkText: 'View Grok plugins docs', + }, { key: 'kimi', label: 'Kimi Code', diff --git a/apps/docs/features/ui/AgentPluginsPanel.tsx b/apps/docs/features/ui/AgentPluginsPanel.tsx index c5ebcf3e43713..7df6f067d13a6 100644 --- a/apps/docs/features/ui/AgentPluginsPanel.tsx +++ b/apps/docs/features/ui/AgentPluginsPanel.tsx @@ -150,6 +150,26 @@ function PluginInstructions({ client }: { client: PluginClient }) { ) } + if (client.key === 'grok') { + return ( +
+

+ Install the Supabase plugin by running the following command in your terminal. +

+ +

+ Browse and install plugins in a session: run grok, then /plugins{' '} + or /marketplace. +

+
+ ) + } + if (client.key === 'vscode') { return (
@@ -240,7 +260,7 @@ export function AgentPluginsPanel() { rel="noopener noreferrer" className="text-brand-link hover:underline inline-flex items-center" > - View {selectedClient.label} extensions docs + {selectedClient.docsLinkText ?? `View ${selectedClient.label} extensions docs`}
diff --git a/apps/docs/public/img/icons/agent-grok-icon-light.svg b/apps/docs/public/img/icons/agent-grok-icon-light.svg new file mode 100644 index 0000000000000..8ac6121911a2c --- /dev/null +++ b/apps/docs/public/img/icons/agent-grok-icon-light.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/docs/public/img/icons/agent-grok-icon.svg b/apps/docs/public/img/icons/agent-grok-icon.svg new file mode 100644 index 0000000000000..1b71ff1ab24f9 --- /dev/null +++ b/apps/docs/public/img/icons/agent-grok-icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/docs/public/img/icons/agent-warp-icon-light.svg b/apps/docs/public/img/icons/agent-warp-icon-light.svg new file mode 100644 index 0000000000000..ea1212cbc6420 --- /dev/null +++ b/apps/docs/public/img/icons/agent-warp-icon-light.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/docs/public/img/icons/agent-warp-icon.svg b/apps/docs/public/img/icons/agent-warp-icon.svg new file mode 100644 index 0000000000000..9799154c284ad --- /dev/null +++ b/apps/docs/public/img/icons/agent-warp-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/ui-patterns/src/McpUrlBuilder/assets/grok-icon-dark.svg b/packages/ui-patterns/src/McpUrlBuilder/assets/grok-icon-dark.svg new file mode 100644 index 0000000000000..1b71ff1ab24f9 --- /dev/null +++ b/packages/ui-patterns/src/McpUrlBuilder/assets/grok-icon-dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/ui-patterns/src/McpUrlBuilder/assets/grok-icon.svg b/packages/ui-patterns/src/McpUrlBuilder/assets/grok-icon.svg new file mode 100644 index 0000000000000..8ac6121911a2c --- /dev/null +++ b/packages/ui-patterns/src/McpUrlBuilder/assets/grok-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 b6b60aae141f8..996f4157b86d1 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, + GrokMcpConfig, KimiMcpConfig, McpClientBaseConfig, McpClientConfig, @@ -153,6 +154,23 @@ export const MCP_CLIENT_DATA: McpClientData[] = [ } }, }, + { + key: 'grok', + label: 'Grok', + icon: 'grok', + hasDistinctDarkIcon: true, + configFile: '~/.grok/config.toml', + externalDocsUrl: 'https://docs.x.ai/build/features/mcp-servers', + transformConfig: (config): GrokMcpConfig => { + return { + mcp_servers: { + supabase: { + url: config.mcpServers.supabase.url, + }, + }, + } + }, + }, { key: 'kimi', label: 'Kimi Code', @@ -375,6 +393,9 @@ export const MCP_CLI_COMMANDS: Record = { install: (url) => `codex mcp add supabase --url "${url}"`, authenticate: 'codex mcp login supabase', }, + grok: { + install: (url) => `grok mcp add supabase "${url}" --transport http`, + }, 'gemini-cli': { install: (url) => `gemini mcp add -t http supabase "${url}"`, authenticate: '/mcp auth supabase', @@ -397,7 +418,7 @@ export const MCP_CLI_COMMANDS: Record = { export const MCP_CLIENT_GROUPS = [ { heading: 'AI Agent CLI', - keys: ['claude-code', 'codex', 'gemini-cli', 'copilot-cli', 'opencode', 'factory'], + keys: ['claude-code', 'codex', 'grok', 'gemini-cli', 'copilot-cli', 'opencode', 'factory'], }, { heading: 'Web Clients', diff --git a/packages/ui-patterns/src/McpUrlBuilder/clients.instructions.md.tsx b/packages/ui-patterns/src/McpUrlBuilder/clients.instructions.md.tsx index d768b97f66028..f45aa6bedcdac 100644 --- a/packages/ui-patterns/src/McpUrlBuilder/clients.instructions.md.tsx +++ b/packages/ui-patterns/src/McpUrlBuilder/clients.instructions.md.tsx @@ -88,6 +88,28 @@ export const MCP_CLIENT_INSTRUCTIONS: Record = { ), }, + grok: { + primary: ({ url }) => ( + <> + Add the Supabase MCP server to Grok: + + + ), + alternate: () => ( + <> + + The command writes the server to your user config ( + + ), making it available across all your projects. Start Grok and complete the Supabase + OAuth flow when prompted on first use. + + + Verify the connection by running inside a Grok session, or{' '} + from your terminal. + + + ), + }, kimi: { alternate: () => ( <> diff --git a/packages/ui-patterns/src/McpUrlBuilder/types.ts b/packages/ui-patterns/src/McpUrlBuilder/types.ts index 97a03ae103b4d..f1bdc14acc3ed 100644 --- a/packages/ui-patterns/src/McpUrlBuilder/types.ts +++ b/packages/ui-patterns/src/McpUrlBuilder/types.ts @@ -135,6 +135,19 @@ export interface CodexMcpConfig { } } +/** + * Configuration format for Grok CLI MCP client. + * Grok reads a TOML config (`~/.grok/config.toml`) and, like Codex, keys + * servers under a `mcp_servers` table. HTTP transport is inferred from the URL. + */ +export interface GrokMcpConfig { + mcp_servers: { + supabase: { + url: string + } + } +} + /** * Configuration format for Gemini CLI MCP client. * Uses httpUrl instead of url to match Gemini CLI's expected format. @@ -199,6 +212,7 @@ export type McpClientConfig = | FactoryMcpConfig | GeminiMcpConfig | GooseMcpConfig + | GrokMcpConfig | KimiMcpConfig | McpClientBaseConfig | OpenCodeMcpConfig diff --git a/packages/ui-patterns/src/McpUrlBuilder/utils/mcpIconAssets.ts b/packages/ui-patterns/src/McpUrlBuilder/utils/mcpIconAssets.ts index 9b9ea2d02dd49..7795c45bd6a7a 100644 --- a/packages/ui-patterns/src/McpUrlBuilder/utils/mcpIconAssets.ts +++ b/packages/ui-patterns/src/McpUrlBuilder/utils/mcpIconAssets.ts @@ -13,6 +13,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 grokDarkIcon from '../assets/grok-icon-dark.svg' +import grokIcon from '../assets/grok-icon.svg' import kimiDarkIcon from '../assets/kimi-icon-dark.svg' import kimiIcon from '../assets/kimi-icon.svg' import kiroIcon from '../assets/kiro-icon.svg' @@ -44,6 +46,7 @@ const MCP_CLIENT_ICON_ASSETS = { factory: { light: factoryIcon, dark: factoryDarkIcon }, 'gemini-cli': { light: geminiCliIcon, dark: geminiCliIcon }, goose: { light: gooseIcon, dark: gooseDarkIcon }, + grok: { light: grokIcon, dark: grokDarkIcon }, kimi: { light: kimiIcon, dark: kimiDarkIcon }, kiro: { light: kiroIcon, dark: kiroIcon }, openai: { light: openaiIcon, dark: openaiDarkIcon }, From 8c745017fba6383d27c6ddf0e9b621f9f9048441 Mon Sep 17 00:00:00 2001 From: Miranda Limonczenko Date: Wed, 19 Aug 2026 07:29:37 -0700 Subject: [PATCH 4/7] chore(a11y): have CodeRabbit flag live regions, keyboard, motion, and alt text (#49216) Closes FE-3811 ## Problem CodeRabbit reviews UI PRs without prompting on accessibility gaps axe-core cannot judge: live regions, keyboard and hover, reduced motion, alt quality, focus visibility, color-only state, and vague link names. ## Solution - Add a path_instruction on `{apps,packages}/**/*.{tsx,jsx,css,mdx}`. - Keep comments advisory. Skip tests, generated files, Radix/shadcn from `ui`, and mechanical axe findings. - Cover live-region lifecycle, pointer-only and hover-only UI, reduced motion, alt quality including a two-sentence length heuristic, focus rings, color-only state, and generic link names. ## Manual testing 1. After merge, open a PR that touches a UI or MDX file under `apps/` or `packages/`. 2. Confirm CodeRabbit comments on at least one of: an unannounced status change, a live region created with its message, a pointer-only or hover-only control, animation without reduced motion, generic or redundant or long alt, `outline-none` without a focus-visible replacement, color-only status, or a "learn more" link that does not name its destination. ## Summary by CodeRabbit * **Accessibility** * Expanded accessibility review coverage for interface content and styling. * Reviews now identify missing focus indicators, color-only status or selection cues, and unclear link labels. * Continued checks cover state announcements, pointer-only interactions, reduced-motion support, and alternative text quality. --------- Co-authored-by: Cursor --- .coderabbit.yaml | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/.coderabbit.yaml b/.coderabbit.yaml index 33ffc26b371d0..9830ed0274abf 100644 --- a/.coderabbit.yaml +++ b/.coderabbit.yaml @@ -69,6 +69,54 @@ reviews: for both runtimes until the final cleanup pass (tracked in FE-3106). Keep this a reminder to verify, not a hard blocker: if no mirror is required, say so briefly rather than forcing a change. + - path: '{apps,packages}/**/*.{tsx,jsx,css,mdx}' + instructions: | + When reviewing UI changes, flag these accessibility gaps. Comments are + advisory. One comment per gap. Skip test files (*.test.*, *.spec.*) and + generated files. Skip Radix/shadcn primitives imported from ui for all + checks below. Do not flag issues axe-core already catches mechanically, + such as a missing alt attribute, an empty button or link name, or + invalid ARIA. + - State changes: if sighted users can see a status change (toast, + loading/empty swap, copy confirmation, async result) and nothing + announces it, suggest aria-live="polite" or role="status". Reserve + role="alert" for urgent errors or warnings. Skip if a live region, + Radix Toast, or Sonner is already there, or if the change is + decoration only. If a live region is created in the same conditional + as its message, flag that: the region must already exist in the DOM, + then receive the update, or screen readers often announce nothing. + - Mouse interaction: flag pointer-only handlers on a non-interactive + element (div, span, or similar) with no keyboard equivalent. The + listed handlers are illustrative: onClick, onMouseEnter, + onDoubleClick, onContextMenu, onPointerDown, onPointerUp, + onTouchStart, onTouchEnd, and equivalents. Also flag hover-only UI + (content revealed with onMouseEnter or CSS :hover) that has no focus + or keyboard path. + - Animation: flag animate-*, keyframes, or JS motion with no + reduced-motion treatment. Prefer Tailwind motion-reduce: / + motion-safe:, or matchMedia('(prefers-reduced-motion: reduce)'). + packages/config/css/utilities.css only zeroes out .shimmer under + reduced motion, not all animation. + - Alt text: flag generic values such as Image, Icon, Photo, Picture, or + the filename. Flag alt that starts with "image of" or "picture of". + If adjacent visible text already names the image (blog thumbnail next + to its title, icon next to its label), flag it as redundant and + recommend alt="" plus aria-hidden on the image. For a decorative SVG + next to visible text, recommend aria-hidden on the SVG. If alt is + longer than about two sentences, suggest moving the extra into a + caption, adjacent text, or aria-describedby. Do not treat a character + count as a hard fail. + - Focus visibility: flag outline-none, outline-hidden, outline: none, + outline: 0, or equivalent :focus resets that are not paired with a + focus-visible ring or outline, or with the focus-ring or focus-inset + utility. + - Color-only state: flag status, validation, or selection that is + conveyed only by color. Suggest a text label, icon, or sr-only text + in addition. + - Link purpose: flag an accessible name that is only "click here", + "read more", or "learn more" when it does not describe the + destination. Skip if aria-label or wrapping context already names + where the link goes. # Applies our internal engineering skills (.claude/skills/) as CodeRabbit review # guidelines. The skills are the single source of truth — they are consumed From 452227e5d25046fadae0e82551e479e5ffea4901 Mon Sep 17 00:00:00 2001 From: Donna Alexandra <75121406+donna-156@users.noreply.github.com> Date: Wed, 19 Aug 2026 16:22:17 +0100 Subject: [PATCH 5/7] Add Donna Alexandra to humans.txt (#49258) Part of my onboarding to add myself to humans.txt ## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? Docs update to add new joiner (me!) ## What is the current behavior? N/A ## What is the new behavior? I am part of the team. :) ## Additional context Part of the onboarding process. --- apps/docs/public/humans.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/docs/public/humans.txt b/apps/docs/public/humans.txt index 4366f56b139a2..39096065e680b 100644 --- a/apps/docs/public/humans.txt +++ b/apps/docs/public/humans.txt @@ -90,6 +90,7 @@ Dimitrios Liappis Div Arora Divit D Divya Sharma +Donna Alexandra Douglas Hunley Eduardo Gurgel Eleftheria Trivyzaki From bb094f96c84a2679c5fc555892133f46e302ee29 Mon Sep 17 00:00:00 2001 From: Miranda Limonczenko Date: Wed, 19 Aug 2026 08:56:09 -0700 Subject: [PATCH 6/7] docs(mcp): revise authentication note to match style guide (#49219) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Screenshot 2026-08-18 at 12 17 18 PM ## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? Docs update. Copy and dedupe. ## What is the current behavior? Gave this a style edit. Basically, saw this note breaking a lot of style rules at once (`login` instead of `log in`, future tense, and also breaking timelessness) and couldn't help myself for submitting a revision. 😅 ## What is the new behavior? Preview: https://docs-git-cursor-revise-mcp-auth-note-bbe8-supabase.vercel.app/docs/guides/ai-tools/mcp --------- Co-authored-by: Cursor Agent Co-authored-by: Miranda Limonczenko --- apps/docs/features/ui/McpConfigPanel.tsx | 14 +++----------- .../src/McpUrlBuilder/McpConfigPanel.md.tsx | 12 +++--------- .../ui-patterns/src/McpUrlBuilder/clients.data.ts | 9 +++++++++ packages/ui-patterns/src/McpUrlBuilder/index.ts | 1 + 4 files changed, 16 insertions(+), 20 deletions(-) diff --git a/apps/docs/features/ui/McpConfigPanel.tsx b/apps/docs/features/ui/McpConfigPanel.tsx index f7bece0a60d4d..9a8ae8cdf8b50 100644 --- a/apps/docs/features/ui/McpConfigPanel.tsx +++ b/apps/docs/features/ui/McpConfigPanel.tsx @@ -26,6 +26,7 @@ import { import { Admonition } from 'ui-patterns/Admonition' import { createMcpCopyHandler, + MCP_HOSTED_AUTH_NOTE, McpConfigPanel as McpConfigPanelBase, type McpClient, } from 'ui-patterns/McpUrlBuilder' @@ -336,17 +337,8 @@ export function McpConfigPanel() { /> {isPlatform && ( - -

- { - "Some MCP clients will automatically prompt you to login during setup, while others may require manual authentication steps. Either authentication method will open a browser window where you can login to your Supabase account and grant organization access to the MCP client. In the future, we'll offer more fine grain control over these permissions." - } -

-

- { - 'Previously Supabase MCP required you to generate a personal access token (PAT), but this is no longer required.' - } -

+ +

{MCP_HOSTED_AUTH_NOTE.body}

)} diff --git a/packages/ui-patterns/src/McpUrlBuilder/McpConfigPanel.md.tsx b/packages/ui-patterns/src/McpUrlBuilder/McpConfigPanel.md.tsx index 87b23d3fe4104..3f2753185a0e9 100644 --- a/packages/ui-patterns/src/McpUrlBuilder/McpConfigPanel.md.tsx +++ b/packages/ui-patterns/src/McpUrlBuilder/McpConfigPanel.md.tsx @@ -9,6 +9,7 @@ import { DEFAULT_MCP_URL_NON_PLATFORM as LOCAL_URL, MCP_CLIENT_DATA, MCP_CLIENT_GROUPS, + MCP_HOSTED_AUTH_NOTE, } from './clients.data' import type { McpClientData } from './clients.data' import { @@ -144,16 +145,9 @@ export function McpConfigPanel() { ))} - Authentication - - - Some MCP clients automatically prompt you to log in during setup, while others require - manual authentication steps. Either way, a browser window opens where you log in to your - Supabase account and grant the MCP client access to your organization. - - - A personal access token (PAT) was previously required, but is no longer needed. + {MCP_HOSTED_AUTH_NOTE.title} + {MCP_HOSTED_AUTH_NOTE.body} ) } diff --git a/packages/ui-patterns/src/McpUrlBuilder/clients.data.ts b/packages/ui-patterns/src/McpUrlBuilder/clients.data.ts index 996f4157b86d1..972815f37b566 100644 --- a/packages/ui-patterns/src/McpUrlBuilder/clients.data.ts +++ b/packages/ui-patterns/src/McpUrlBuilder/clients.data.ts @@ -439,3 +439,12 @@ export const DEFAULT_MCP_URL_NON_PLATFORM = 'http://localhost:54321/mcp' * docs document (and what to fall back to in production). */ export const HOSTED_MCP_URL = 'https://mcp.supabase.com/mcp' + +/** + * Hosted MCP authentication note. Shared by the docs HTML callout and the + * markdown export so the two pipelines cannot drift. + */ +export const MCP_HOSTED_AUTH_NOTE = { + title: 'Authentication', + body: "Some MCP clients automatically prompt you to log in during setup. Others require manual authentication steps. Either method opens a browser window where you log in to your Supabase account and grant the MCP client access to your organization. You don't need a personal access token (PAT).", +} as const diff --git a/packages/ui-patterns/src/McpUrlBuilder/index.ts b/packages/ui-patterns/src/McpUrlBuilder/index.ts index fe8d3b9680007..aea7e6a4d5c7e 100644 --- a/packages/ui-patterns/src/McpUrlBuilder/index.ts +++ b/packages/ui-patterns/src/McpUrlBuilder/index.ts @@ -9,6 +9,7 @@ export { FEATURE_GROUPS_NON_PLATFORM, MCP_CLIENT_GROUPS, MCP_CLIENT_DATA, + MCP_HOSTED_AUTH_NOTE, } from './clients.data' export type { McpClientData } from './clients.data' export { MCP_CLIENTS } from './mcpClients' From 4343e21da0315760b287c0a7a814b92f0d506a11 Mon Sep 17 00:00:00 2001 From: Charis <26616127+charislam@users.noreply.github.com> Date: Wed, 19 Aug 2026 12:11:10 -0400 Subject: [PATCH 7/7] feat(studio): tighten the notebook diff preview (#49218) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? UI refactor of the notebook create/update preview in the AI Assistant panel, plus a small additive prop on the shared `CodeBlock`. ## What is the current behavior? The assistant's notebook diff renders each cell as its own bordered box with a gap between them, under a `6 cells` line that is easy to miss. Cells can't be collapsed, each one carries a repeated `ADDED` badge and a nested "Show more" toggle, and long markdown scrolls sideways instead of wrapping. ## What is the new behavior? CleanShot 2026-08-18 at 14 41 30@2x - The whole diff is one card: a distinct header row (notebook name, summary, expand/collapse all) over cells glued together by dividers. - Every cell is a `Collapsible`. Added and replaced cells open by default; unchanged, moved, and removed cells stay as single rows but are now inspectable instead of being content-free. - The per-row badge is replaced by a colored gutter glyph (`+` `−` `~` `↕`) with a tooltip naming the change type. The change type reaches the accessible name via `aria-label` on the row. - The nested "Show more" toggle inside each cell is gone — the row itself is the only control. - `CodeBlock` gains a `wrapLongLines` prop (default `false`, no change for existing callers), used here so markdown and SQL soft-wrap. The highlighter sets `white-space` inline on the `` element, so a class on the `
` can't do this.

## Additional context

Towards FE-4143


## Summary by CodeRabbit

* **New Features**
* Notebook previews now display titles, notebook icons, and clearer
bordered layouts.
* Added per-cell expand/collapse controls, including “Expand all” and
“Collapse all.”
  * Long code lines can now wrap for improved readability.

* **Improvements**
* Added mode-based fallback labels when notebook titles are unavailable.
* Newly added and replaced cells expand by default, while unchanged
cells remain collapsed.
* Improved change markers, tooltips, removed-cell styling, and notebook
proposal preview spacing.

---
 .../NotebookPreview/NotebookPreview.tsx       |  86 +++++--
 .../NotebookPreview/NotebookPreview.utils.ts  |   9 +
 .../NotebookPreview/NotebookPreviewCell.tsx   | 215 ++++++++++--------
 .../NotebookProposalRenderer.tsx              |  40 +++-
 .../ui-patterns/src/CodeBlock/CodeBlock.tsx   |   4 +
 5 files changed, 231 insertions(+), 123 deletions(-)

diff --git a/apps/studio/components/interfaces/Explorer/NotebookPreview/NotebookPreview.tsx b/apps/studio/components/interfaces/Explorer/NotebookPreview/NotebookPreview.tsx
index 62b8754e1e1ad..5d79e016bef5a 100644
--- a/apps/studio/components/interfaces/Explorer/NotebookPreview/NotebookPreview.tsx
+++ b/apps/studio/components/interfaces/Explorer/NotebookPreview/NotebookPreview.tsx
@@ -1,9 +1,11 @@
+import { NotebookPen } from 'lucide-react'
 import { useState } from 'react'
-import { Button } from 'ui'
+import { Button, cn } from 'ui'
 
 import {
   formatNotebookDiffSummary,
   getEntryKey,
+  isEntryExpandedByDefault,
   summarizeNotebookDiff,
 } from './NotebookPreview.utils'
 import { NotebookPreviewCell } from './NotebookPreviewCell'
@@ -12,34 +14,92 @@ import type { NotebookCellDiffEntry } from '@/data/content/notebooks/notebook-op
 export interface NotebookPreviewProps {
   entries: NotebookCellDiffEntry[]
   mode: 'create' | 'update'
+  /** The notebook's name, shown in the card header. Falls back to a generic label. */
+  title?: string
+  className?: string
 }
 
 const VISIBLE_ENTRY_LIMIT = 5
 
+const FALLBACK_TITLE = {
+  create: 'New notebook',
+  update: 'Notebook changes',
+} as const
+
 /**
  * Read-only preview of a proposed notebook create/update, rendered from a pre-computed diff.
  * Pure presentational component: no data fetching, no approval or notebook-editor state — see
  * `deriveNotebookDiff` for how `entries` is produced.
  */
-export const NotebookPreview = ({ entries, mode }: NotebookPreviewProps) => {
-  const [isExpanded, setIsExpanded] = useState(false)
+export const NotebookPreview = ({ entries, mode, title, className }: NotebookPreviewProps) => {
+  const [isShowingAllEntries, setIsShowingAllEntries] = useState(false)
+  const [expandedOverrides, setExpandedOverrides] = useState>({})
 
   const summary = summarizeNotebookDiff(entries, mode)
-  const visibleEntries = isExpanded ? entries : entries.slice(0, VISIBLE_ENTRY_LIMIT)
+  const visibleEntries = isShowingAllEntries ? entries : entries.slice(0, VISIBLE_ENTRY_LIMIT)
   const hiddenCount = entries.length - visibleEntries.length
 
+  const isExpanded = (entry: NotebookCellDiffEntry) =>
+    expandedOverrides[getEntryKey(entry)] ?? isEntryExpandedByDefault(entry)
+
+  const hasEntries = entries.length > 0
+  const areAllExpanded = visibleEntries.every(isExpanded)
+
+  const toggleAll = () =>
+    setExpandedOverrides(
+      Object.fromEntries(entries.map((entry) => [getEntryKey(entry), !areAllExpanded]))
+    )
+
   return (
-    
-

- {formatNotebookDiffSummary(summary)} -

-
- {visibleEntries.map((entry) => ( - - ))} +
+
+ + + {title ?? FALLBACK_TITLE[mode]} + + + {formatNotebookDiffSummary(summary)} + + {hasEntries && ( + + )} +
+
+ {visibleEntries.map((entry) => { + const key = getEntryKey(entry) + return ( + + setExpandedOverrides((prev) => ({ ...prev, [key]: open })) + } + /> + ) + })}
{hiddenCount > 0 && ( - )} diff --git a/apps/studio/components/interfaces/Explorer/NotebookPreview/NotebookPreview.utils.ts b/apps/studio/components/interfaces/Explorer/NotebookPreview/NotebookPreview.utils.ts index 4dd3087ffc8e0..3d40260a6e87b 100644 --- a/apps/studio/components/interfaces/Explorer/NotebookPreview/NotebookPreview.utils.ts +++ b/apps/studio/components/interfaces/Explorer/NotebookPreview/NotebookPreview.utils.ts @@ -17,6 +17,15 @@ export function getEntryKey(entry: NotebookCellDiffEntry): string { } } +/** + * Whether a diff entry starts expanded. Only the entries the user has to actually read to + * decide — the ones whose content the assistant is proposing — open on their own; unchanged, + * moved, and removed cells stay as single rows until asked for. + */ +export function isEntryExpandedByDefault(entry: NotebookCellDiffEntry): boolean { + return entry._tag === 'added' || entry._tag === 'replaced' +} + /** Human label for a collapsed/badge row. */ export function getCellLabel(cell: CellWire | AgentCell): string { switch (cell._tag) { diff --git a/apps/studio/components/interfaces/Explorer/NotebookPreview/NotebookPreviewCell.tsx b/apps/studio/components/interfaces/Explorer/NotebookPreview/NotebookPreviewCell.tsx index ddac9ae9e1cb6..67153618e7b15 100644 --- a/apps/studio/components/interfaces/Explorer/NotebookPreview/NotebookPreviewCell.tsx +++ b/apps/studio/components/interfaces/Explorer/NotebookPreview/NotebookPreviewCell.tsx @@ -1,6 +1,14 @@ -import { useState, type ReactNode } from 'react' -import { Badge, Button, cn } from 'ui' -import { CodeBlock, type CodeBlockLang } from 'ui-patterns/CodeBlock' +import { ChevronRight } from 'lucide-react' +import { + cn, + Collapsible, + CollapsibleContent, + CollapsibleTrigger, + Tooltip, + TooltipContent, + TooltipTrigger, +} from 'ui' +import { CodeBlock } from 'ui-patterns/CodeBlock' import { getCellCodeBlockLanguage, @@ -15,77 +23,100 @@ import type { AgentCell, CellWire } from '@/data/content/notebooks/notebook-sche export interface NotebookPreviewCellProps { entry: NotebookCellDiffEntry + isExpanded: boolean + onExpandedChange: (isExpanded: boolean) => void } -/** Renders a single diff entry, dispatching on its tag. */ -export const NotebookPreviewCell = ({ entry }: NotebookPreviewCellProps) => { - switch (entry._tag) { - case 'unchanged': - return - case 'removed': - return ( - - ) - case 'moved': - return ( - - ) - case 'added': - return - case 'replaced': - return - } -} +/** + * A single-character gutter marker per change type The glyph is decorative — + * `changeLabel` carries the same information into the row's accessible name and its tooltip. + */ +const CHANGE_MARKERS = { + added: { glyph: '+', changeLabel: 'Added', className: 'text-brand-600' }, + removed: { glyph: '−', changeLabel: 'Removed', className: 'text-destructive' }, + replaced: { glyph: '~', changeLabel: 'Replaced', className: 'text-warning-600' }, + moved: { glyph: '↕', changeLabel: 'Moved', className: 'text-foreground-lighter' }, + unchanged: { glyph: '', changeLabel: 'Unchanged', className: '' }, +} as const + +type ChangeMarker = (typeof CHANGE_MARKERS)[keyof typeof CHANGE_MARKERS] -interface CollapsedRowProps { - label: string - strikethrough?: boolean - badge?: { variant: 'destructive' | 'secondary'; label: string } +/** The cell a row is labelled by — for a replacement, the proposed cell rather than the current one. */ +function getEntryCell(entry: NotebookCellDiffEntry): CellWire | AgentCell { + return entry._tag === 'replaced' ? entry.after : entry.cell } -/** A single muted row with no content — used for unchanged, removed, and moved entries. */ -const CollapsedRow = ({ label, strikethrough, badge }: CollapsedRowProps) => ( -
- {badge && {badge.label}} - - {label} - -
-) +/** One row of the diff card: a header line that collapses to a single row, plus its content. */ +export const NotebookPreviewCell = ({ + entry, + isExpanded, + onExpandedChange, +}: NotebookPreviewCellProps) => { + const marker = CHANGE_MARKERS[entry._tag] + const isRemoved = entry._tag === 'removed' + const label = getCellLabel(getEntryCell(entry)) -interface ContentCellProps { - badge: { variant: 'success' | 'warning'; label: string } - label: string - children: ReactNode + return ( + + + + + + {label} + + + +
+ +
+
+
+ ) } -/** Shared frame for entries that show full cell content — added and replaced cells. */ -const ContentCell = ({ badge, label, children }: ContentCellProps) => ( -
-
- {badge.label} - {label} -
- {children} -
-) +/** + * The gutter glyph, with a tooltip naming the change type it stands for. The glyph stays + * `aria-hidden` — the row's `aria-label` already carries the same word — so the tooltip is a + * pointer affordance rather than a second announcement. + */ +const ChangeGlyph = ({ marker }: { marker: ChangeMarker }) => { + const className = cn('w-3 shrink-0 text-center font-mono text-xs leading-none', marker.className) -const AddedCell = ({ cell }: { cell: AgentCell }) => ( - - - - -) + // An unchanged cell has no glyph, so there is nothing to explain — render the spacer alone. + if (!marker.glyph) return + + return ( + + + + {marker.glyph} + + + {marker.changeLabel} + + ) +} + +const CellBody = ({ entry }: { entry: NotebookCellDiffEntry }) => + entry._tag === 'replaced' ? ( + + ) : ( + <> + + + + ) /** * A `replace_cell` can change only the source parameters (`database_identifier`, @@ -93,12 +124,12 @@ const AddedCell = ({ cell }: { cell: AgentCell }) => ( * change at all, so the metadata is compared independently and rendered as its own * before → after line whenever it differs. */ -const ReplacedCell = ({ before, after }: { before: CellWire; after: AgentCell }) => { +const ReplacedCellBody = ({ before, after }: { before: CellWire; after: AgentCell }) => { const beforeMetadata = getCellMetadataLine(before) const afterMetadata = getCellMetadataLine(after) return ( - + <> )} - + ) } -/** A plain-text metadata line for a query cell — never rendered as a link or attribute. */ -const MetadataLine = ({ text }: { text: string | null }) => - text ?

{text}

: null - -interface ExpandableCodeBlockProps { - language: CodeBlockLang - value: string -} - /** - * `CodeBlock` clipped to a fixed height with a "Show more/less" toggle. `CodeBlock`'s - * wrapper already scrolls (`overflow-auto`), so clipping just changes what's visible. + * The cell's source, always rendered as literal text via `CodeBlock` — agent-authored markdown + * must never be interpreted into real DOM nodes. */ -const ExpandableCodeBlock = ({ language, value }: ExpandableCodeBlockProps) => { - const [isExpanded, setIsExpanded] = useState(false) +const CellSource = ({ cell }: { cell: CellWire | AgentCell }) => ( + +) - return ( -
- - -
- ) -} +/** A plain-text metadata line for a query cell — never rendered as a link or attribute. */ +const MetadataLine = ({ text }: { text: string | null }) => + text ?

{text}

: null diff --git a/apps/studio/components/ui/AIAssistantPanel/NotebookProposalRenderer.tsx b/apps/studio/components/ui/AIAssistantPanel/NotebookProposalRenderer.tsx index b644d8aeafb67..18a38634a89a4 100644 --- a/apps/studio/components/ui/AIAssistantPanel/NotebookProposalRenderer.tsx +++ b/apps/studio/components/ui/AIAssistantPanel/NotebookProposalRenderer.tsx @@ -1,7 +1,7 @@ import { useParams } from 'common' import { Loader2 } from 'lucide-react' import Link from 'next/link' -import { Button } from 'ui' +import { Button, cn } from 'ui' import { Admonition } from 'ui-patterns/Admonition' import { CodeBlock } from 'ui-patterns/CodeBlock' @@ -118,6 +118,16 @@ interface NotebookConfirmFooterProps { onDeny?: () => void } +/** + * `ConfirmFooter` is built to sit flush under the block it confirms (`border-t-0 rounded-b-lg`), + * so that block has to square off its own bottom corners and the two must not be gapped apart. + */ +const GLUED_TO_FOOTER = 'rounded-b-none' + +function hasConfirmFooter(state: NotebookProposalState) { + return state === 'approval-requested' || state === 'approval-responded' +} + /** The footer morphs (label + disabled) across approval-requested/approval-responded and is absent otherwise. */ function NotebookConfirmFooter({ mode, @@ -129,7 +139,7 @@ function NotebookConfirmFooter({ onApprove, onDeny, }: NotebookConfirmFooterProps) { - if (state !== 'approval-requested' && state !== 'approval-responded') return null + if (!hasConfirmFooter(state)) return null const copy = MODE_COPY[mode] const isApprovalRequested = state === 'approval-requested' @@ -155,7 +165,7 @@ function NotebookParseFailure({ onDeny, }: Pick) { return ( -
+
- +
+
) @@ -243,7 +258,7 @@ function UpdateNotebookProposal({ state, input, onApprove, onDeny }: NotebookPro if (isError || !notebook) { return ( -
+
{(state === 'approval-requested' || state === 'approval-responded') && (