From 99c1cb77815be5232af989ff22344595f950b863 Mon Sep 17 00:00:00 2001
From: yyh <92089059+lyzno1@users.noreply.github.com>
Date: Sun, 26 Jul 2026 15:08:48 +0800
Subject: [PATCH] fix(web): prevent search hotkey hydration mismatch (#39574)
---
.../__tests__/search-button.spec.tsx | 34 +++++++++++++++++++
.../main-nav/components/search-button.tsx | 22 ++++++++++--
2 files changed, 54 insertions(+), 2 deletions(-)
create mode 100644 web/app/components/main-nav/components/__tests__/search-button.spec.tsx
diff --git a/web/app/components/main-nav/components/__tests__/search-button.spec.tsx b/web/app/components/main-nav/components/__tests__/search-button.spec.tsx
new file mode 100644
index 00000000000..0e8d74c1a72
--- /dev/null
+++ b/web/app/components/main-nav/components/__tests__/search-button.spec.tsx
@@ -0,0 +1,34 @@
+import { act, waitFor, within } from '@testing-library/react'
+import { hydrateRoot } from 'react-dom/client'
+import { renderToString } from 'react-dom/server'
+import { MainNavSearchButton } from '../search-button'
+
+describe('MainNavSearchButton', () => {
+ afterEach(() => {
+ vi.restoreAllMocks()
+ })
+
+ it('renders Ctrl during SSR and corrects it after Mac hydration without a mismatch', async () => {
+ vi.spyOn(window.navigator, 'platform', 'get').mockReturnValue('MacIntel')
+ const app =
+ const container = document.createElement('div')
+ container.innerHTML = renderToString(app)
+ const getSearchButton = () =>
+ within(container).getByRole('button', { name: 'app.gotoAnything.searchTitle' })
+
+ expect(getSearchButton()).toHaveTextContent('CtrlK')
+ expect(getSearchButton()).not.toHaveTextContent('⌘')
+
+ const onRecoverableError = vi.fn()
+ const root = hydrateRoot(container, app, {
+ onRecoverableError,
+ })
+
+ try {
+ await waitFor(() => expect(getSearchButton()).toHaveTextContent('⌘K'))
+ expect(onRecoverableError).not.toHaveBeenCalled()
+ } finally {
+ act(() => root.unmount())
+ }
+ })
+})
diff --git a/web/app/components/main-nav/components/search-button.tsx b/web/app/components/main-nav/components/search-button.tsx
index 1539d38369f..c8378e1f79c 100644
--- a/web/app/components/main-nav/components/search-button.tsx
+++ b/web/app/components/main-nav/components/search-button.tsx
@@ -2,13 +2,31 @@
import { DialogTrigger } from '@langgenius/dify-ui/dialog'
import { Kbd } from '@langgenius/dify-ui/kbd'
-import { formatForDisplay } from '@tanstack/react-hotkeys'
+import { detectPlatform, formatForDisplay } from '@tanstack/react-hotkeys'
+import { useSyncExternalStore } from 'react'
import { useTranslation } from 'react-i18next'
import { gotoAnythingDialogHandle } from '@/app/components/goto-anything/dialog-handle'
import { GOTO_ANYTHING_HOTKEY } from '@/app/components/goto-anything/hotkeys'
+function noopSubscribe() {
+ return () => {}
+}
+
+function getPlatformSnapshot() {
+ return detectPlatform()
+}
+
+function getServerPlatformSnapshot(): ReturnType {
+ return 'linux'
+}
+
+function useDisplayPlatform() {
+ return useSyncExternalStore(noopSubscribe, getPlatformSnapshot, getServerPlatformSnapshot)
+}
+
export function MainNavSearchButton() {
const { t } = useTranslation()
+ const displayPlatform = useDisplayPlatform()
return (
{GOTO_ANYTHING_HOTKEY.split('+').map((key) => (
- {formatForDisplay(key)}
+ {formatForDisplay(key, { platform: displayPlatform })}
))}